channel is close

This commit is contained in:
jbannon 2022-04-09 07:42:24 +00:00
parent 764678367f
commit a1ccb0669e
10 changed files with 148 additions and 37 deletions

View file

@ -43,3 +43,31 @@ presets:
title: "{title}"
album: "Music Videos"
year: "{upload_year}"
yt_channel:
youtube:
ytdl_options:
format: 'best'
ignoreerrors: True
output_options:
file_name: "{output_file_name}.{ext}"
convert_thumbnail: "jpeg"
thumbnail_name: "{output_file_name}.jpeg"
metadata_options:
nfo:
nfo_name: "{output_file_name}.nfo"
nfo_root: "episodedetails"
tags:
title: "{title}"
season: "{upload_year}"
episode: "{upload_month}{upload_day_padded}"
plot: "{description}"
year: "{upload_year}"
aired: "{upload_year}-{upload_month_padded}-{upload_day_padded}"
output_directory_nfo:
nfo_name: "tvshow.nfo"
nfo_root: "tvshow"
tags:
title: "{tv_show_name}"
overrides:
output_file_name: "Season {upload_year}/s{upload_year}.e{upload_month_padded}{upload_day_padded} - {sanitized_title}"

View file

@ -48,12 +48,24 @@
# overrides:
# artist: "Rammstein"
gogo_video_test:
preset: "music_videos"
#gogo_video_test:
# preset: "music_videos"
# youtube:
# download_strategy: "video"
# video_id: "2RHTiXvELNg"
# output_options:
# output_directory: "/tmp/gogos"
# overrides:
# artist: "The Gogos"
recent_channel_test:
preset: "yt_channel"
ytdl_options:
daterange: now-2weeks
youtube:
download_strategy: "video"
video_id: "2RHTiXvELNg"
download_strategy: "channel"
channel_id: "UCRcCVDu_4oARsAKFkKYydAQ"
output_options:
output_directory: "/tmp/gogos"
output_directory: "/tmp/dunkey"
overrides:
artist: "The Gogos"
tv_show_name: "Youtube: Dunkey"

View file

@ -23,6 +23,11 @@ class YoutubeDownloader(Downloader):
"""Returns full video url"""
return f"https://youtube.com/watch?v={video_id}"
@classmethod
def channel_url(cls, channel_id: str) -> str:
"""Returns full channel url"""
return f"https://youtube.com/channel/{channel_id}"
def _download_with_metadata(self, url: str) -> None:
"""
Do not get entries from the extract info, let it write to the info.json file and load
@ -36,12 +41,13 @@ class YoutubeDownloader(Downloader):
_ = self.extract_info(ytdl_options_overrides=ytdl_metadata_override, url=url)
def download_video(self, video_id: str) -> YoutubeVideo:
"""Download a single Youtube video"""
entry = self.extract_info(url=self.video_url(video_id))
return YoutubeVideo(**entry)
def download_playlist(self, playlist_id: str) -> List[YoutubeVideo]:
"""
Downloads all videos in a playlist
Downloads all videos in a Youtube playlist
"""
playlist_url = self.playlist_url(playlist_id=playlist_id)
@ -57,3 +63,23 @@ class YoutubeDownloader(Downloader):
entries.append(YoutubeVideo(**json.load(file)))
return entries
def download_channel(self, channel_id: str) -> List[YoutubeVideo]:
"""
Downloads all videos from a channel
TODO: Add caching via ids in the metadata. Scrape output directory for any vid ids and
TODO: include it in the archive
"""
self._download_with_metadata(url=self.channel_url(channel_id))
# Load the entries from info.json
entries: List[YoutubeVideo] = []
# Load the entries from info.json
# TODO dupe code between this and playlist
for file_name in os.listdir(self.output_directory):
if file_name.endswith(".info.json") and not file_name.startswith(channel_id):
with open(Path(self.output_directory) / file_name, "r", encoding="utf-8") as file:
entries.append(YoutubeVideo(**json.load(file)))
return entries

View file

@ -62,6 +62,30 @@ class Entry:
"""Returns the entry's upload year"""
return int(self.upload_date[:4])
@property
def upload_month_padded(self) -> str:
"""Returns the entry's upload month, padded"""
return self.upload_date[4:6]
@property
def upload_day_padded(self) -> str:
"""Returns the entry's upload day, padded"""
return self.upload_date[6:8]
@property
def upload_month(self) -> int:
"""Returns the entry's upload month as an int"""
return int(self.upload_month_padded.lstrip("0"))
@property
def upload_day(self) -> int:
"""Returns the entry's upload month as an int"""
return int(self.upload_day_padded.lstrip("0"))
@property
def description(self) -> str:
return self.kwargs("description")
@property
def thumbnail(self) -> str:
"""Returns the entry's thumbnail url"""

View file

@ -2,7 +2,7 @@ import os
from abc import ABC
from pathlib import Path
from shutil import copyfile
from typing import Generic
from typing import Generic, overload, Optional
from typing import Type
from typing import TypeVar
@ -19,6 +19,7 @@ from ytdl_subscribe.validators.config.config_options.config_options_validator im
from ytdl_subscribe.validators.config.metadata_options.metadata_options_validator import (
MetadataOptionsValidator,
)
from ytdl_subscribe.validators.config.metadata_options.nfo_validator import NFOValidator
from ytdl_subscribe.validators.config.output_options.output_options_validator import (
OutputOptionsValidator,
)
@ -81,20 +82,14 @@ class Subscription(Generic[S], ABC):
"""Returns the directory that the downloader saves files to"""
return str(Path(self.__config_options.working_directory.value) / Path(self.name))
def _apply_formatter(self, entry: Entry, formatter: StringFormatterValidator) -> str:
def _apply_formatter(self, formatter: StringFormatterValidator, entry: Optional[Entry] = None) -> str:
"""
Parameters
----------
entry
Entry with values to use in the formatter
formatter
The formatter itself
Returns
-------
The format_string after .format has been called on it using entry and override values
Returns the format_string after .format has been called on it using entry (if provided) and
override values
"""
variable_dict = dict(entry.to_dict(), **self.overrides.dict_with_format_strings)
variable_dict = self.overrides.dict_with_format_strings
if entry:
variable_dict = dict(entry.to_dict(), **variable_dict)
return formatter.apply_formatter(variable_dict)
def _post_process_tagging(self, entry: Entry):
@ -104,21 +99,20 @@ class Subscription(Generic[S], ABC):
id3_options = self.metadata_options.id3
audio_file = music_tag.load_file(entry.file_path(relative_directory=self.working_directory))
for tag, tag_formatter in id3_options.tags.dict.items():
audio_file[tag] = self._apply_formatter(entry=entry, formatter=tag_formatter)
audio_file[tag] = self._apply_formatter(formatter=tag_formatter, entry=entry)
audio_file.save()
def _post_process_nfo(self, entry: Entry):
def _post_process_nfo(self, nfo_options: NFOValidator, entry: Optional[Entry] = None):
"""
Creates an entry's NFO file using values defined in the metadata options
"""
nfo = {}
nfo_options = self.metadata_options.nfo
for tag, tag_formatter in nfo_options.tags.dict.items():
nfo[tag] = self._apply_formatter(entry=entry, formatter=tag_formatter)
nfo[tag] = self._apply_formatter(formatter=tag_formatter, entry=entry)
# Write the nfo tags to XML with the nfo_root
nfo_root = self._apply_formatter(entry=entry, formatter=nfo_options.nfo_root)
nfo_root = self._apply_formatter(formatter=nfo_options.nfo_root, entry=entry)
xml = dicttoxml.dicttoxml(
obj=nfo,
root=True, # We assume all NFOs have a root. Maybe we should not?
@ -126,9 +120,9 @@ class Subscription(Generic[S], ABC):
attr_type=False,
)
nfo_file_name = self._apply_formatter(entry=entry, formatter=nfo_options.nfo_name)
nfo_file_name = self._apply_formatter(formatter=nfo_options.nfo_name, entry=entry)
output_directory = self._apply_formatter(
entry=entry, formatter=self.output_options.output_directory
formatter=self.output_options.output_directory, entry=entry
)
# Save the nfo's XML to file
@ -150,11 +144,11 @@ class Subscription(Generic[S], ABC):
entry_source_file_path = entry.file_path(relative_directory=self.working_directory)
output_directory = self._apply_formatter(
entry=entry, formatter=self.output_options.output_directory
formatter=self.output_options.output_directory, entry=entry
)
output_file_name = self._apply_formatter(
entry=entry, formatter=self.output_options.file_name
formatter=self.output_options.file_name, entry=entry
)
entry_destination_file_path = Path(output_directory) / Path(output_file_name)
@ -166,7 +160,7 @@ class Subscription(Generic[S], ABC):
source_thumbnail_path = entry.thumbnail_path(relative_directory=self.working_directory)
output_thumbnail_name = self._apply_formatter(
entry=entry, formatter=self.output_options.thumbnail_name
formatter=self.output_options.thumbnail_name, entry=entry
)
output_thumbnail_path = Path(output_directory) / Path(output_thumbnail_name)
@ -185,4 +179,7 @@ class Subscription(Generic[S], ABC):
copyfile(source_thumbnail_path, output_thumbnail_path)
if self.metadata_options.nfo:
self._post_process_nfo(entry)
self._post_process_nfo(nfo_options=self.metadata_options.nfo, entry=entry)
if self.metadata_options.output_directory_nfo:
self._post_process_nfo(nfo_options=self.metadata_options.output_directory_nfo)

View file

@ -1,7 +1,7 @@
from ytdl_subscribe.downloaders.youtube_downloader import YoutubeDownloader
from ytdl_subscribe.subscriptions.subscription import Subscription
from ytdl_subscribe.validators.config.source_options.youtube_validators import (
YoutubePlaylistSourceValidator,
YoutubePlaylistSourceValidator, YoutubeChannelSourceValidator,
)
from ytdl_subscribe.validators.config.source_options.youtube_validators import (
YoutubeVideoSourceValidator,
@ -18,6 +18,16 @@ class YoutubePlaylistSubscription(Subscription[YoutubePlaylistSourceValidator]):
self.post_process_entry(entry)
class YoutubeChannelSubscription(Subscription[YoutubeChannelSourceValidator]):
def extract_info(self):
entries = self.get_downloader(YoutubeDownloader).download_channel(
channel_id=self.source_options.channel_id.value
)
for entry in entries:
self.post_process_entry(entry)
class YoutubeVideoSubscription(Subscription[YoutubeVideoSourceValidator]):
def extract_info(self):
entry = self.get_downloader(YoutubeDownloader).download_video(

View file

@ -4,10 +4,13 @@ from ytdl_subscribe.validators.config.metadata_options.nfo_validator import NFOV
class MetadataOptionsValidator(StrictDictValidator):
_optional_keys = {"id3", "nfo"}
_optional_keys = {"id3", "nfo", "output_directory_nfo"}
def __init__(self, name, value):
super().__init__(name, value)
self.id3 = self._validate_key_if_present(key="id3", validator=Id3Validator)
self.nfo = self._validate_key_if_present(key="nfo", validator=NFOValidator)
# TODO: Ensure this does not depend on entry variables, only overrides
self.output_directory_nfo = self._validate_key_if_present(key="output_directory_nfo", validator=NFOValidator)

View file

@ -11,7 +11,7 @@ from ytdl_subscribe.validators.config.source_options.soundcloud_validators impor
)
from ytdl_subscribe.validators.config.source_options.source_validators import SourceValidator
from ytdl_subscribe.validators.config.source_options.youtube_validators import (
YoutubePlaylistSourceValidator,
YoutubePlaylistSourceValidator, YoutubeChannelSourceValidator,
)
from ytdl_subscribe.validators.config.source_options.youtube_validators import (
YoutubeVideoSourceValidator,
@ -65,6 +65,7 @@ class SoundcloudDownloadStrategyValidator(DownloadStrategyValidator):
class YoutubeDownloadStrategyValidator(DownloadStrategyValidator):
_download_strategy_to_source_mapping = {
"channel": YoutubeChannelSourceValidator,
"playlist": YoutubePlaylistSourceValidator,
"video": YoutubeVideoSourceValidator,
}

View file

@ -16,3 +16,11 @@ class YoutubeVideoSourceValidator(YoutubeSourceValidator):
def __init__(self, name, value):
super().__init__(name, value)
self.video_id = self._validate_key("video_id", StringValidator)
class YoutubeChannelSourceValidator(YoutubeSourceValidator):
_required_keys = {"channel_id"}
def __init__(self, name, value):
super().__init__(name, value)
self.channel_id = self._validate_key("channel_id", StringValidator)

View file

@ -9,7 +9,8 @@ from mergedeep import mergedeep
from ytdl_subscribe.subscriptions.soundcloud import SoundcloudAlbumsAndSinglesSubscription
from ytdl_subscribe.subscriptions.subscription import Subscription
from ytdl_subscribe.subscriptions.youtube import YoutubePlaylistSubscription
from ytdl_subscribe.subscriptions.youtube import YoutubePlaylistSubscription, \
YoutubeChannelSubscription
from ytdl_subscribe.subscriptions.youtube import YoutubeVideoSubscription
from ytdl_subscribe.validators.base.strict_dict_validator import StrictDictValidator
from ytdl_subscribe.validators.base.validators import StringValidator
@ -22,7 +23,7 @@ from ytdl_subscribe.validators.config.source_options.soundcloud_validators impor
SoundcloudAlbumsAndSinglesSourceValidator,
)
from ytdl_subscribe.validators.config.source_options.youtube_validators import (
YoutubePlaylistSourceValidator,
YoutubePlaylistSourceValidator, YoutubeChannelSourceValidator,
)
from ytdl_subscribe.validators.config.source_options.youtube_validators import (
YoutubeVideoSourceValidator,
@ -78,7 +79,8 @@ class SubscriptionValidator(StrictDictValidator):
subscription_class = YoutubePlaylistSubscription
elif isinstance(self.preset.subscription_source, YoutubeVideoSourceValidator):
subscription_class = YoutubeVideoSubscription
elif isinstance(self.preset.subscription_source, YoutubeChannelSourceValidator):
subscription_class = YoutubeChannelSubscription
if subscription_class is None:
raise ValueError("subscription source class not found")