diff --git a/docs/conf.py b/docs/conf.py index 4102001e..d603eaa7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -49,6 +49,8 @@ exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # html_theme = "sphinx_rtd_theme" +html_theme_options = {"navigation_depth": 10} + # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". @@ -56,6 +58,6 @@ html_theme = "sphinx_rtd_theme" # Do not show full module path in api docs add_module_names = False -python_use_unqualified_type_names = True +python_use_unqualified_type_names = False napoleon_numpy_docstrings = True napoleon_use_rtype = False diff --git a/docs/config.rst b/docs/config.rst new file mode 100644 index 00000000..056b8abe --- /dev/null +++ b/docs/config.rst @@ -0,0 +1,198 @@ +Config +====== +ytdl-sub is configured using a ``config.yaml`` file. You can view our +:doc:`examples ` and read detailed documentation for every configurable +field below. + +The ``config.yaml`` is made up of two sections: + +.. code-block:: yaml + + configuration: + presets: + +You can jump to any section and subsection of the config using the navigation +section to the left. + +configuration +------------- +The ``configuration`` section contains app-wide configs applied to all presets +and subscriptions. + +presets +------- +``presets`` define a `formula` for how to format downloaded media and metadata. + +download_strategy +^^^^^^^^^^^^^^^^^ +Download strategies dictate what is getting downloaded from a source. Each +download strategy has its own set of parameters. + +youtube +""""""" + +channel +''''''' +.. code-block:: yaml + + presets: + my_example_preset: + youtube: + download_strategy: "channel" + +channel_id +__________ + .. autoproperty:: ytdl_sub.downloaders.youtube_downloader.YoutubeChannelDownloaderOptions.channel_id + +channel_avatar_path +___________________ + .. autoproperty:: ytdl_sub.downloaders.youtube_downloader.YoutubeChannelDownloaderOptions.channel_avatar_path + +channel_banner_path +___________________ + .. autoproperty:: ytdl_sub.downloaders.youtube_downloader.YoutubeChannelDownloaderOptions.channel_banner_path + +before +______ + .. autoproperty:: ytdl_sub.downloaders.youtube_downloader.YoutubeChannelDownloaderOptions.before + +after +_____ + .. autoproperty:: ytdl_sub.downloaders.youtube_downloader.YoutubeChannelDownloaderOptions.after + +------------------------------------------------------------------------------- + +playlist +'''''''' +.. code-block:: yaml + + presets: + my_example_preset: + youtube: + download_strategy: "playlist" + +playlist_id +___________ + .. autoproperty:: ytdl_sub.downloaders.youtube_downloader.YoutubePlaylistDownloaderOptions.playlist_id + +------------------------------------------------------------------------------- + +video +''''' +.. code-block:: yaml + + presets: + my_example_preset: + youtube: + download_strategy: "video" + +video_id +________ + .. autoproperty:: ytdl_sub.downloaders.youtube_downloader.YoutubeVideoDownloaderOptions.video_id + +------------------------------------------------------------------------------- + +soundcloud +"""""""""" + +albums_and_singles +'''''''''''''''''' +.. code-block:: yaml + + presets: + my_example_preset: + soundcloud: + download_strategy: "albums_and_singles" + +username +________ + .. autoproperty:: ytdl_sub.downloaders.soundcloud_downloader.SoundcloudAlbumsAndSinglesDownloadOptions.username + +skip_premiere_tracks +____________________ + .. autoproperty:: ytdl_sub.downloaders.soundcloud_downloader.SoundcloudAlbumsAndSinglesDownloadOptions.skip_premiere_tracks + +------------------------------------------------------------------------------- + +output_options +^^^^^^^^^^^^^^ + +output_directory +"""""""""""""""" + .. autoproperty:: ytdl_sub.config.preset_options.OutputOptions.output_directory + +file_name +""""""""" + .. autoproperty:: ytdl_sub.config.preset_options.OutputOptions.file_name + +thumbnail_name +"""""""""""""" + .. autoproperty:: ytdl_sub.config.preset_options.OutputOptions.thumbnail_name + +maintain_download_archive +""""""""""""""""""""""""" + .. autoproperty:: ytdl_sub.config.preset_options.OutputOptions.maintain_download_archive + +keep_files +"""""""""" + .. autoproperty:: ytdl_sub.config.preset_options.OutputOptions.keep_files + +------------------------------------------------------------------------------- + +ytdl_options +^^^^^^^^^^^^ +.. autoclass:: ytdl_sub.config.preset_options.YTDLOptions() + +------------------------------------------------------------------------------- + +overrides +^^^^^^^^^ +.. autoclass:: ytdl_sub.config.preset_options.Overrides() + +Plugins +^^^^^^^ + +music_tags +"""""""""" +.. autoclass:: ytdl_sub.plugins.music_tags.MusicTagsOptions() + +nfo +""" +.. autoclass:: ytdl_sub.plugins.nfo_tags.NfoTagsOptions() + +nfo_output_directory +"""""""""""""""""""" +.. autoclass:: ytdl_sub.plugins.output_directory_nfo_tags.OutputDirectoryNfoTagsOptions() + +Source Variables +---------------- +Source variables are ``{variables}`` that contain metadata from downloaded media. +These variables can be used in StringFormatters, but not OverrideFormatters. + +Youtube Variables +^^^^^^^^^^^^^^^^^ +.. automodule:: ytdl_sub.entries.variables.youtube_variables + :members: + :inherited-members: + :undoc-members: + +Soundcloud Variables +^^^^^^^^^^^^^^^^^^^^ +.. automodule:: ytdl_sub.entries.variables.soundcloud_variables + :members: + :inherited-members: + :undoc-members: + +Formatters +---------- +Formatters are strings that can contain ``{variables}`` that are overwritten at +run-time with values assigned to that variable. There are two different types of +formatters. + +String Formatter +^^^^^^^^^^^^^^^^ +.. autoclass:: ytdl_sub.validators.string_formatter_validators.StringFormatterValidator() + +Overrides Formatter +^^^^^^^^^^^^^^^^^^^ +.. autoclass:: ytdl_sub.validators.string_formatter_validators.OverridesStringFormatterValidator() diff --git a/docs/config/examples/index.rst b/docs/config/examples/index.rst deleted file mode 100644 index 2f3a6915..00000000 --- a/docs/config/examples/index.rst +++ /dev/null @@ -1,10 +0,0 @@ -Examples -======== - -Hello, WIP - -.. toctree:: - :titlesonly: - :maxdepth: 2 - - kodi_tv_shows diff --git a/docs/config/examples/kodi_tv_shows.rst b/docs/config/examples/kodi_tv_shows.rst deleted file mode 100644 index 91895320..00000000 --- a/docs/config/examples/kodi_tv_shows.rst +++ /dev/null @@ -1,12 +0,0 @@ -Kodi/Jellyfin TV Shows -====================== - -Config - -.. include:: ../../../examples/kodi_tv_shows_config.yaml - :literal: - - - -.. include:: ../../../examples/kodi_tv_shows_subscriptions.yaml - :literal: diff --git a/docs/config/format_variables/index.rst b/docs/config/format_variables/index.rst deleted file mode 100644 index 207bcc13..00000000 --- a/docs/config/format_variables/index.rst +++ /dev/null @@ -1,11 +0,0 @@ -Format Variables -================ - -Hello, WIP - -.. toctree:: - :titlesonly: - :maxdepth: 2 - - soundcloud - youtube diff --git a/docs/config/format_variables/soundcloud.rst b/docs/config/format_variables/soundcloud.rst deleted file mode 100644 index 4c9e7540..00000000 --- a/docs/config/format_variables/soundcloud.rst +++ /dev/null @@ -1,7 +0,0 @@ -Soundcloud Track ----------------- - -.. automodule:: ytdl_sub.entries.variables.soundcloud_variables - :members: - :inherited-members: - :undoc-members: diff --git a/docs/config/format_variables/youtube.rst b/docs/config/format_variables/youtube.rst deleted file mode 100644 index 8fdb9e39..00000000 --- a/docs/config/format_variables/youtube.rst +++ /dev/null @@ -1,7 +0,0 @@ -Youtube Video -------------- - -.. automodule:: ytdl_sub.entries.variables.youtube_variables - :members: - :inherited-members: - :undoc-members: diff --git a/docs/config/index.rst b/docs/config/index.rst deleted file mode 100644 index d0dc34f0..00000000 --- a/docs/config/index.rst +++ /dev/null @@ -1,11 +0,0 @@ -Configuration -============= - -Hello, WIP - -.. toctree:: - :titlesonly: - :maxdepth: 2 - - format_variables/index - examples/index diff --git a/docs/examples.rst b/docs/examples.rst new file mode 100644 index 00000000..d1838daa --- /dev/null +++ b/docs/examples.rst @@ -0,0 +1,48 @@ +Examples +======== + +This page shows how you can use ytdl-sub for various use cases. These +are the configs I personally use and have incorporated as part of the e2e tests. + +Each example has a ``config.yaml`` and ``subscription.yaml``. The config defines +`how` you format your media, whereas the subscription defines `what` you +download plus some additional configuring if needed. + +Also note that ``kodi`` examples are applicable for ``jellyfin``, ``emby``, and +``plex`` with the +`XBMC Movies `_ +or +`XBMC TV Show `_ +Plex importer. We would like to improve Plex support, please chime in +`here `_ +if you have experience with importing custom videos with metadata. + +Kodi/Jellyfin TV Shows +---------------------- + +config.yaml +^^^^^^^^^^^ + +.. include:: ../examples/kodi_tv_shows_config.yaml + :literal: + +subscriptions.yaml +^^^^^^^^^^^^^^^^^^ + +.. include:: ../examples/kodi_tv_shows_subscriptions.yaml + :literal: + +Kodi/Jellyfin Music Videos +-------------------------- + +config.yaml +^^^^^^^^^^^ + +.. include:: ../examples/kodi_music_videos_config.yaml + :literal: + +subscriptions.yaml +^^^^^^^^^^^^^^^^^^ + +.. include:: ../examples/kodi_music_videos_subscriptions.yaml + :literal: \ No newline at end of file diff --git a/docs/getting_started.rst b/docs/getting_started.rst new file mode 100644 index 00000000..ff78131e --- /dev/null +++ b/docs/getting_started.rst @@ -0,0 +1,58 @@ +Getting Started +=============== + +The goal of this app is automate the downloading and metadata creation of audio and video files, then place the files +in a directory that gets read by your media player/server. Everyone stores and watches their media differently, so +we strive for comprehensive and simplistic customization to fit all self-hosting needs. + +Audio metadata is just a matter of adding tags to the audio file. The backend plugin we use supports practically every +common audio file type, so there should (hopefully) be no issues getting audio recognized by your media player. + +Video metadata on the other hand, is currently geared toward generating Kodi/Jellyfin/Emby NFO files. Plex uses metadata +written within MP4 containers - we do not support that currently, but could be added as a plugin in the future. + +Usage Ideas +----------- +Below is a list ways you can use ytdl-sub to download and consume different kinds of media in the representation you +prefer. + +* Download a Youtube channel + * Store the channel as its own TV Show + * Use the channel's avatar as the TV Show poster, banner as the fanart + * Format the season and episodes as ``Season YYYY/sYYYY.eMMDD - Video Title.mp4`` to easily navigate videos by upload date + * Store the channel as a single season under a TV Show shared with other channels + * Only download audio, store it as a Podcast + * Only keep video/audio uploaded in the last `N` days + * Great for news or podcast based channels + +* Download a Youtube playlist + * Download an artist's music videos playlist, store each video as a Kodi/Jellyfin/Emby Music Video + * Only download the audio, store the playlist as an album + +* Manually download a single Youtube video + * Store it as a Movie + * Download a one-hit wonder and store it as a Kodi/Jellyfin/Emby Music Video + +* Download a soundcloud artist's discography + * Add tags and album cover images so it shows up nicely in your music player + +If you want to jump the gun to see how ytdl-sub can be configured to do these things, head over to the +:doc:`examples `. + +Install +------- +Once we are ready for our first release, we will add this package to pypi. Then, +we plan to create a docker image that uses the +`LinuxServer.io `_ +base image, and hopefully become a part of their fleet someday. + +Until then, you will have to clone this repo and run it using python 3.10 + +.. code-block:: Bash + + git clone https://github.com/jmbannon/ytdl-sub.git + cd ytdl-sub + + pip install -e . + + diff --git a/docs/getting_started/index.rst b/docs/getting_started/index.rst deleted file mode 100644 index 7d80396e..00000000 --- a/docs/getting_started/index.rst +++ /dev/null @@ -1,19 +0,0 @@ -Getting Started -=============== - -The goal of this app is automate the downloading and metadata creation of audio and video files, then place the files -in a directory that gets read by your media player/server. Everyone stores and watches their media differently, so -we strive for comprehensive and simplistic customization to fit all self-hosting needs. - -Audio metadata is just a matter of adding tags to the audio file. The backend plugin we use supports practically every -common audio file type, so there should (hopefully) be no issues getting audio recognized by your media player. - -Video metadata on the other hand, is currently geared toward generating Kodi/Jellyfin/Emby NFO files. Plex uses metadata -written within MP4 containers - we do not support that currently, but could be added as a plugin in the future. - -.. toctree:: - :titlesonly: - :maxdepth: 2 - - usage_ideas - install diff --git a/docs/getting_started/install.rst b/docs/getting_started/install.rst deleted file mode 100644 index 50db1a92..00000000 --- a/docs/getting_started/install.rst +++ /dev/null @@ -1,2 +0,0 @@ -Install -======= \ No newline at end of file diff --git a/docs/getting_started/usage_ideas.rst b/docs/getting_started/usage_ideas.rst deleted file mode 100644 index 98e08a2d..00000000 --- a/docs/getting_started/usage_ideas.rst +++ /dev/null @@ -1,28 +0,0 @@ -Usage Ideas -=========== -Below is a list ways you can use ytdl-sub to download and consume different kinds of media in the representation you -prefer. - -* Download a Youtube channel - * Store the channel as its own TV Show - * Use the channel's avatar as the TV Show poster, banner as the fanart - * Format the season and episodes as ``Season YYYY/sYYYY.eMMDD - Video Title.mp4`` to easily navigate videos by upload date - * Store the channel as a single season under a TV Show shared with other channels - * Only download audio, store it as a Podcast - * Only keep video/audio uploaded in the last `N` days - * Great for news or podcast based channels - -* Download a Youtube playlist - * Download an artist's music videos playlist, store each video as a Kodi/Jellyfin/Emby Music Video - * Only download the audio, store the playlist as an album - -* Manually download a single Youtube video - * Store it as a Movie - * Download a one-hit wonder and store it as a Kodi/Jellyfin/Emby Music Video - -* Download a soundcloud artist's discography - * Add tags and album cover images so it shows up nicely in your music player - -If you want to jump the gun to see how ytdl-sub can be configured to do these things, head over to the -:doc:`examples <../config/examples/index>`. - diff --git a/docs/index.rst b/docs/index.rst index 3036833c..8b3be2de 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,16 +6,17 @@ python app that strives to automate downloading media and preparing it for consumption in your favorite media player in the most hassle-free way possible. -If you are new to ytdl-sub, head over to the Getting Started page. This guide -shows how you can install ytdl-sub, configure it to download various types -of media, and start downloading. +If you are new to ytdl-sub, head over to the +:doc:`Getting Started ` +page. This guide shows how you can install ytdl-sub, configure it to download +various types of media, and start downloading. Contents ======== .. toctree:: - :titlesonly: - :maxdepth: 2 + :maxdepth: 10 - getting_started/index - config/index + getting_started + config + examples diff --git a/src/ytdl_sub/config/preset_options.py b/src/ytdl_sub/config/preset_options.py index 7e60d9bf..ca827c97 100644 --- a/src/ytdl_sub/config/preset_options.py +++ b/src/ytdl_sub/config/preset_options.py @@ -13,12 +13,52 @@ from ytdl_sub.validators.validators import LiteralDictValidator class YTDLOptions(LiteralDictValidator): - """Ensures `ytdl_options` is a dict""" + """ + Optional. This section allows you to add any ytdl argument to ytdl-sub's downloader. + The argument names can differ slightly from the command-line argument names. See + `this docstring `_ + for more details. + + ytdl_options should be formatted like: + + .. code-block:: yaml + + presets: + my_example_preset: + ytdl_options: + ignoreerrors: True + + where each key is a ytdl argument. + """ +# Disable for proper docstring formatting +# pylint: disable=line-too-long class Overrides(DictFormatterValidator): - """Ensures `overrides` is a dict""" + """ + Optional. This section allows you to define variables that can be used in any string formatter. + For example, if you want your file and thumbnail files to match without copy-pasting a large + format string, you can define something like: + .. code-block:: yaml + + presets: + my_example_preset: + overrides: + output_directory: "/path/to/media" + custom_file_name: "{upload_year}.{upload_month_padded}.{upload_day_padded}.{sanitized_title}" + + # Then use the override variables in the output options + output_options: + output_directory: "{output_directory}" + file_name: "{custom_file_name}.{ext}" + thumbnail_name: "{custom_file_name}.{thumbnail_ext}" + + Override variables can contain explicit values and other variables, including both override + and source variables. + """ + + # pylint: enable=line-too-long def __init__(self, name, value): super().__init__(name, value) for key in self._keys: @@ -60,26 +100,98 @@ class OutputOptions(StrictDictValidator): # Output directory should resolve without any entry variables. # This is to check the directory for any download-archives before any downloads begin - self.output_directory: OverridesStringFormatterValidator = self._validate_key( + self._output_directory: OverridesStringFormatterValidator = self._validate_key( key="output_directory", validator=OverridesStringFormatterValidator ) # file name and thumbnails however can use entry variables - self.file_name: StringFormatterValidator = self._validate_key( + self._file_name: StringFormatterValidator = self._validate_key( key="file_name", validator=StringFormatterValidator ) - self.thumbnail_name = self._validate_key_if_present( + self._thumbnail_name = self._validate_key_if_present( key="thumbnail_name", validator=StringFormatterValidator ) - self.maintain_download_archive = self._validate_key_if_present( + self._maintain_download_archive = self._validate_key_if_present( key="maintain_download_archive", validator=BoolValidator, default=False ) - self.delete_stale_files = self._validate_key_if_present( + self._keep_files = self._validate_key_if_present( key="keep_files", validator=DateRangeValidator ) - if self.delete_stale_files and not self.maintain_download_archive: + if self._keep_files and not self.maintain_download_archive: raise self._validation_exception( "keep_files requires maintain_download_archive set to True" ) + + @property + def output_directory(self) -> OverridesStringFormatterValidator: + """ + Required. The output directory to store all media files downloaded. + """ + return self._output_directory + + @property + def file_name(self) -> StringFormatterValidator: + """ + Required. The file name for the media file. This can include directories such as + ``"Season {upload_year}/{title}.{ext}"``, and will be placed in the output directory. + """ + return self._file_name + + @property + def thumbnail_name(self) -> Optional[StringFormatterValidator]: + """ + Optional. The file name for the media's thumbnail image. This can include directories such + as ``"Season {upload_year}/{title}.{thumbnail_ext}"``, and will be placed in the output + directory. + """ + return self._thumbnail_name + + @property + def maintain_download_archive(self) -> bool: + """ + Optional. Maintains a download archive file in the output directory for a subscription. + It is named ``.ytdl-sub-{subscription_name}-download-archive.json``, stored in the + output directory. + + The download archive contains a mapping of ytdl IDs to downloaded files. This is used to + create a ytdl download-archive file when invoking a download on a subscription. This will + prevent ytdl from redownloading media already downloaded. + + Defaults to False. + """ + return self._maintain_download_archive.value + + @property + def keep_files(self) -> DateRangeValidator: + """ + Optional. Requires ``maintain_download_archive`` set to True. + + Only keeps files that are uploaded in the defined range. Should be formatted as: + + .. code-block:: yaml + + presets: + my_example_preset: + output_options: + keep_files: + before: + after: + + where ``before`` and ``after`` are date-times. A common usage of this option is to only + fill in the after, such as: + + .. code-block:: yaml + + presets: + my_example_preset: + output_options: + keep_files: + after: today-2weeks + + Which translates to 'keep files uploaded in the last two weeks'. + + By default, ytdl-sub will keep all files. + """ + return self._keep_files diff --git a/src/ytdl_sub/downloaders/soundcloud_downloader.py b/src/ytdl_sub/downloaders/soundcloud_downloader.py index 8b0509d9..23f1a2e6 100644 --- a/src/ytdl_sub/downloaders/soundcloud_downloader.py +++ b/src/ytdl_sub/downloaders/soundcloud_downloader.py @@ -24,10 +24,17 @@ class SoundcloudDownloaderOptions(DownloaderValidator, ABC): def __init__(self, name: str, value: dict): super().__init__(name=name, value=value) - self.skip_premiere_tracks = self._validate_key( + self._skip_premiere_tracks = self._validate_key( "skip_premiere_tracks", BoolValidator, default=True ) + @property + def skip_premiere_tracks(self) -> bool: + """ + Optional. True to skip tracks that require purchasing. False otherwise. Defaults to True. + """ + return self._skip_premiere_tracks.value + SoundcloudDownloaderOptionsT = TypeVar( "SoundcloudDownloaderOptionsT", bound=SoundcloudDownloaderOptions @@ -68,7 +75,14 @@ class SoundcloudAlbumsAndSinglesDownloadOptions(SoundcloudDownloaderOptions): def __init__(self, name, value): super().__init__(name, value) - self.username = self._validate_key(key="username", validator=StringValidator) + self._username = self._validate_key(key="username", validator=StringValidator) + + @property + def username(self) -> str: + """ + Required. The Soundcloud username found in the url of their page. + """ + return self._username.value class SoundcloudAlbumsAndSinglesDownloader( @@ -132,7 +146,7 @@ class SoundcloudAlbumsAndSinglesDownloader( """ Soundcloud subscription to download albums and tracks as singles. """ - artist_url = self.artist_url(artist_name=self.download_options.username.value) + artist_url = self.artist_url(artist_name=self.download_options.username) entry_dicts = self.extract_info_via_info_json(url=artist_url) # Get all of the artist's albums @@ -146,7 +160,7 @@ class SoundcloudAlbumsAndSinglesDownloader( tracks += album.album_tracks() # Filter any premiere tracks if specified - if self.download_options.skip_premiere_tracks.value: + if self.download_options.skip_premiere_tracks: tracks = [track for track in tracks if not track.is_premiere()] return tracks diff --git a/src/ytdl_sub/downloaders/youtube_downloader.py b/src/ytdl_sub/downloaders/youtube_downloader.py index 8f4ed770..8de267cb 100644 --- a/src/ytdl_sub/downloaders/youtube_downloader.py +++ b/src/ytdl_sub/downloaders/youtube_downloader.py @@ -59,7 +59,15 @@ class YoutubeVideoDownloaderOptions(YoutubeDownloaderOptions): def __init__(self, name, value): super().__init__(name, value) - self.video_id = self._validate_key("video_id", StringValidator) + self._video_id = self._validate_key("video_id", StringValidator) + + @property + def video_id(self) -> str: + """ + Required. The ID of the video. Looks like the ``VMAPTo7RVDo`` in + ``youtube.com/watch?v=VMAPTo7RVDo``. + """ + return self._video_id.value class YoutubeVideoDownloader(YoutubeDownloader[YoutubeVideoDownloaderOptions, YoutubeVideo]): @@ -73,8 +81,7 @@ class YoutubeVideoDownloader(YoutubeDownloader[YoutubeVideoDownloaderOptions, Yo def download(self) -> List[YoutubeVideo]: """Download a single Youtube video""" - video_id = self.download_options.video_id.value - video_url = self.video_url(video_id=video_id) + video_url = self.video_url(video_id=self.download_options.video_id) entry_dict = self.extract_info(url=video_url) return [YoutubeVideo(entry_dict=entry_dict, working_directory=self.working_directory)] @@ -89,7 +96,14 @@ class YoutubePlaylistDownloaderOptions(YoutubeDownloaderOptions): def __init__(self, name, value): super().__init__(name, value) - self.playlist_id = self._validate_key("playlist_id", StringValidator) + self._playlist_id = self._validate_key("playlist_id", StringValidator) + + @property + def playlist_id(self) -> str: + """ + Required. The playlist's ID. + """ + return self._playlist_id.value class YoutubePlaylistDownloader( @@ -107,8 +121,7 @@ class YoutubePlaylistDownloader( """ Downloads all videos in a Youtube playlist """ - playlist_id = self.download_options.playlist_id.value - playlist_url = self.playlist_url(playlist_id=playlist_id) + playlist_url = self.playlist_url(playlist_id=self.download_options.playlist_id) playlist_videos: List[YoutubePlaylistVideo] = [] entry_dicts = self.extract_info_via_info_json(url=playlist_url) @@ -134,14 +147,37 @@ class YoutubeChannelDownloaderOptions(YoutubeDownloaderOptions, DateRangeValidat def __init__(self, name, value): YoutubeDownloaderOptions.__init__(self, name, value) DateRangeValidator.__init__(self, name, value) - self.channel_id = self._validate_key("channel_id", StringValidator) - self.channel_avatar_path = self._validate_key_if_present( + self._channel_id = self._validate_key("channel_id", StringValidator) + self._channel_avatar_path = self._validate_key_if_present( "channel_avatar_path", OverridesStringFormatterValidator ) - self.channel_banner_path = self._validate_key_if_present( + self._channel_banner_path = self._validate_key_if_present( "channel_banner_path", OverridesStringFormatterValidator ) + @property + def channel_id(self) -> str: + """ + Required. The channel's ID. Not to be confused with the username. It should look something + like `UCsvn_Po0SmunchJYOWpOxMg`. You can get this by opening a video and clicking on the + channel's avatar image to take you to their channel, then check the url. + """ + return self._channel_id.value + + @property + def channel_avatar_path(self) -> Optional[OverridesStringFormatterValidator]: + """ + Optional. Path to store the channel's avatar thumbnail image to. + """ + return self._channel_avatar_path + + @property + def channel_banner_path(self) -> Optional[OverridesStringFormatterValidator]: + """ + Optional. Path to store the channel's banner image to. + """ + return self._channel_banner_path + class YoutubeChannelDownloader(YoutubeDownloader[YoutubeChannelDownloaderOptions, YoutubeVideo]): downloader_options_type = YoutubeChannelDownloaderOptions @@ -167,20 +203,11 @@ class YoutubeChannelDownloader(YoutubeDownloader[YoutubeChannelDownloaderOptions """Returns full channel url""" return f"https://youtube.com/channel/{channel_id}" - @property - def channel_id(self) -> str: - """ - Returns - ------- - Channel ID - """ - return self.download_options.channel_id.value - def download(self) -> List[YoutubeVideo]: """ Downloads all videos from a channel """ - channel_url = self.channel_url(channel_id=self.channel_id) + channel_url = self.channel_url(channel_id=self.download_options.channel_id) channel_videos: List[YoutubeVideo] = [] ytdl_options_overrides = {} diff --git a/src/ytdl_sub/subscriptions/subscription.py b/src/ytdl_sub/subscriptions/subscription.py index 54dbe718..11333731 100644 --- a/src/ytdl_sub/subscriptions/subscription.py +++ b/src/ytdl_sub/subscriptions/subscription.py @@ -202,9 +202,9 @@ class Subscription: # If output options maintains stale file deletion, perform the delete here prior to saving # the download archive if self.output_options.maintain_download_archive: - if self.output_options.delete_stale_files: + if self.output_options.keep_files: self._enhanced_download_archive.remove_stale_files( - date_range=self.output_options.delete_stale_files.get_date_range() + date_range=self.output_options.keep_files.get_date_range() ) self._enhanced_download_archive.save_download_mappings() diff --git a/src/ytdl_sub/validators/date_range_validator.py b/src/ytdl_sub/validators/date_range_validator.py index d69a06ac..aa0268bd 100644 --- a/src/ytdl_sub/validators/date_range_validator.py +++ b/src/ytdl_sub/validators/date_range_validator.py @@ -11,8 +11,18 @@ class DateRangeValidator(StrictDictValidator): def __init__(self, name, value): super().__init__(name, value) - self.before = self._validate_key_if_present("before", StringDatetimeValidator) - self.after = self._validate_key_if_present("after", StringDatetimeValidator) + self._before = self._validate_key_if_present("before", StringDatetimeValidator) + self._after = self._validate_key_if_present("after", StringDatetimeValidator) + + @property + def before(self) -> Optional[StringDatetimeValidator]: + """Optional. Only download videos before this datetime.""" + return self._before + + @property + def after(self) -> Optional[StringDatetimeValidator]: + """Optional. Only download videos after this datetime.""" + return self._after def get_date_range(self) -> Optional[DateRange]: """ @@ -20,9 +30,9 @@ class DateRangeValidator(StrictDictValidator): ------- Date range if the 'before' or 'after' is defined. None otherwise. """ - if self.before or self.after: + if self._before or self._after: return DateRange( - start=self.after.datetime_str if self.after else None, - end=self.before.datetime_str if self.before else None, + start=self._after.datetime_str if self._after else None, + end=self._before.datetime_str if self._before else None, ) return None diff --git a/src/ytdl_sub/ytdl_additions/enhanced_download_archive.py b/src/ytdl_sub/ytdl_additions/enhanced_download_archive.py index 50786b89..fe86c95e 100644 --- a/src/ytdl_sub/ytdl_additions/enhanced_download_archive.py +++ b/src/ytdl_sub/ytdl_additions/enhanced_download_archive.py @@ -355,7 +355,7 @@ class EnhancedDownloadArchive: ------- The download mapping's file name (no path) """ - return f".ytdl-subscribe-{self.subscription_name}-download-mapping.json" + return f".ytdl-sub-{self.subscription_name}-download-archive.json" @property def _mapping_output_file_path(self): diff --git a/tests/e2e/youtube/test_channel_as_kodi_tv_show.py b/tests/e2e/youtube/test_channel_as_kodi_tv_show.py index 01d7de32..03c1e22e 100644 --- a/tests/e2e/youtube/test_channel_as_kodi_tv_show.py +++ b/tests/e2e/youtube/test_channel_as_kodi_tv_show.py @@ -66,7 +66,7 @@ def expected_full_channel_download(): return ExpectedDownload( expected_md5_file_hashes={ # Download mapping - Path("pz/.ytdl-subscribe-pz-download-mapping.json"): "add71021318bf87a3facb965fd38bd7f", + Path("pz/.ytdl-sub-pz-download-archive.json"): "add71021318bf87a3facb965fd38bd7f", # Output directory files Path("pz/fanart.jpg"): "e6e323373c8902568e96e374817179cf", @@ -158,7 +158,7 @@ def expected_recent_channel_download(): return ExpectedDownload( expected_md5_file_hashes={ # Download mapping - Path("pz/.ytdl-subscribe-pz-download-mapping.json"): "a133d9ea8a63e239cd41b799b9031fd5", + Path("pz/.ytdl-sub-pz-download-archive.json"): "a133d9ea8a63e239cd41b799b9031fd5", # Output directory files Path("pz/fanart.jpg"): "e6e323373c8902568e96e374817179cf", @@ -205,7 +205,7 @@ def expected_rolling_recent_channel_download(): return ExpectedDownload( expected_md5_file_hashes={ # Download mapping - Path("pz/.ytdl-subscribe-pz-download-mapping.json"): "8013b4d2ba6921c9347c014ac915e3f6", + Path("pz/.ytdl-sub-pz-download-archive.json"): "8013b4d2ba6921c9347c014ac915e3f6", # Output directory files Path("pz/fanart.jpg"): "e6e323373c8902568e96e374817179cf",