diff --git a/docs/conf.py b/docs/conf.py index 67f247bc..e5d7e46c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -57,3 +57,5 @@ html_static_path = ["_static"] # Do not show full module path in api docs add_module_names = False python_use_unqualified_type_names = True +napoleon_numpy_docstrings = True +napoleon_use_rtype = False diff --git a/docs/source/ytdl_subscribe.config.rst b/docs/source/ytdl_subscribe.config.rst new file mode 100644 index 00000000..9468672c --- /dev/null +++ b/docs/source/ytdl_subscribe.config.rst @@ -0,0 +1,53 @@ +ytdl\_subscribe.config package +============================== + +Submodules +---------- + +ytdl\_subscribe.config.config\_file module +------------------------------------------ + +.. automodule:: ytdl_subscribe.config.config_file + :members: + :undoc-members: + :show-inheritance: + +ytdl\_subscribe.config.preset module +------------------------------------ + +.. automodule:: ytdl_subscribe.config.preset + :members: + :undoc-members: + :show-inheritance: + +ytdl\_subscribe.config.preset\_class\_mappings module +----------------------------------------------------- + +.. automodule:: ytdl_subscribe.config.preset_class_mappings + :members: + :undoc-members: + :show-inheritance: + +ytdl\_subscribe.config.preset\_options module +--------------------------------------------- + +.. automodule:: ytdl_subscribe.config.preset_options + :members: + :undoc-members: + :show-inheritance: + +ytdl\_subscribe.config.subscription module +------------------------------------------ + +.. automodule:: ytdl_subscribe.config.subscription + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: ytdl_subscribe.config + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/ytdl_subscribe.entries.rst b/docs/source/ytdl_subscribe.entries.rst index b792c21f..aa3768f8 100644 --- a/docs/source/ytdl_subscribe.entries.rst +++ b/docs/source/ytdl_subscribe.entries.rst @@ -1,9 +1,25 @@ ytdl\_subscribe.entries package =============================== +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + ytdl_subscribe.entries.variables + Submodules ---------- +ytdl\_subscribe.entries.base\_entry module +------------------------------------------ + +.. automodule:: ytdl_subscribe.entries.base_entry + :members: + :undoc-members: + :show-inheritance: + ytdl\_subscribe.entries.entry module ------------------------------------ diff --git a/docs/source/ytdl_subscribe.entries.variables.rst b/docs/source/ytdl_subscribe.entries.variables.rst new file mode 100644 index 00000000..15999e03 --- /dev/null +++ b/docs/source/ytdl_subscribe.entries.variables.rst @@ -0,0 +1,37 @@ +ytdl\_subscribe.entries.variables package +========================================= + +Submodules +---------- + +ytdl\_subscribe.entries.variables.entry\_variables module +--------------------------------------------------------- + +.. automodule:: ytdl_subscribe.entries.variables.entry_variables + :members: + :undoc-members: + :show-inheritance: + +ytdl\_subscribe.entries.variables.soundcloud\_variables module +-------------------------------------------------------------- + +.. automodule:: ytdl_subscribe.entries.variables.soundcloud_variables + :members: + :undoc-members: + :show-inheritance: + +ytdl\_subscribe.entries.variables.youtube\_variables module +----------------------------------------------------------- + +.. automodule:: ytdl_subscribe.entries.variables.youtube_variables + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: ytdl_subscribe.entries.variables + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/ytdl_subscribe.plugins.rst b/docs/source/ytdl_subscribe.plugins.rst new file mode 100644 index 00000000..eee04df7 --- /dev/null +++ b/docs/source/ytdl_subscribe.plugins.rst @@ -0,0 +1,53 @@ +ytdl\_subscribe.plugins package +=============================== + +Submodules +---------- + +ytdl\_subscribe.plugins.convert\_thumbnail module +------------------------------------------------- + +.. automodule:: ytdl_subscribe.plugins.convert_thumbnail + :members: + :undoc-members: + :show-inheritance: + +ytdl\_subscribe.plugins.music\_tags module +------------------------------------------ + +.. automodule:: ytdl_subscribe.plugins.music_tags + :members: + :undoc-members: + :show-inheritance: + +ytdl\_subscribe.plugins.nfo\_tags module +---------------------------------------- + +.. automodule:: ytdl_subscribe.plugins.nfo_tags + :members: + :undoc-members: + :show-inheritance: + +ytdl\_subscribe.plugins.output\_directory\_nfo\_tags module +----------------------------------------------------------- + +.. automodule:: ytdl_subscribe.plugins.output_directory_nfo_tags + :members: + :undoc-members: + :show-inheritance: + +ytdl\_subscribe.plugins.plugin module +------------------------------------- + +.. automodule:: ytdl_subscribe.plugins.plugin + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: ytdl_subscribe.plugins + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/ytdl_subscribe.rst b/docs/source/ytdl_subscribe.rst index e72a6f2e..82a140f7 100644 --- a/docs/source/ytdl_subscribe.rst +++ b/docs/source/ytdl_subscribe.rst @@ -8,9 +8,12 @@ Subpackages :maxdepth: 4 ytdl_subscribe.cli + ytdl_subscribe.config ytdl_subscribe.downloaders ytdl_subscribe.entries + ytdl_subscribe.plugins ytdl_subscribe.subscriptions + ytdl_subscribe.utils ytdl_subscribe.validators ytdl_subscribe.ytdl_additions diff --git a/docs/source/ytdl_subscribe.subscriptions.rst b/docs/source/ytdl_subscribe.subscriptions.rst index 05be67c8..9338ecc9 100644 --- a/docs/source/ytdl_subscribe.subscriptions.rst +++ b/docs/source/ytdl_subscribe.subscriptions.rst @@ -4,14 +4,6 @@ ytdl\_subscribe.subscriptions package Submodules ---------- -ytdl\_subscribe.subscriptions.soundcloud module ------------------------------------------------ - -.. automodule:: ytdl_subscribe.subscriptions.soundcloud - :members: - :undoc-members: - :show-inheritance: - ytdl\_subscribe.subscriptions.subscription module ------------------------------------------------- @@ -20,14 +12,6 @@ ytdl\_subscribe.subscriptions.subscription module :undoc-members: :show-inheritance: -ytdl\_subscribe.subscriptions.youtube module --------------------------------------------- - -.. automodule:: ytdl_subscribe.subscriptions.youtube - :members: - :undoc-members: - :show-inheritance: - Module contents --------------- diff --git a/docs/source/ytdl_subscribe.utils.rst b/docs/source/ytdl_subscribe.utils.rst new file mode 100644 index 00000000..eda7eeb9 --- /dev/null +++ b/docs/source/ytdl_subscribe.utils.rst @@ -0,0 +1,21 @@ +ytdl\_subscribe.utils package +============================= + +Submodules +---------- + +ytdl\_subscribe.utils.exceptions module +--------------------------------------- + +.. automodule:: ytdl_subscribe.utils.exceptions + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: ytdl_subscribe.utils + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/ytdl_subscribe.validators.rst b/docs/source/ytdl_subscribe.validators.rst index 13841962..3813db85 100644 --- a/docs/source/ytdl_subscribe.validators.rst +++ b/docs/source/ytdl_subscribe.validators.rst @@ -1,22 +1,53 @@ ytdl\_subscribe.validators package ================================== -Subpackages ------------ - -.. toctree:: - :maxdepth: 4 - - ytdl_subscribe.validators.base - ytdl_subscribe.validators.config - Submodules ---------- -ytdl\_subscribe.validators.exceptions module +ytdl\_subscribe.validators.date\_range\_validator module +-------------------------------------------------------- + +.. automodule:: ytdl_subscribe.validators.date_range_validator + :members: + :undoc-members: + :show-inheritance: + +ytdl\_subscribe.validators.strict\_dict\_validator module +--------------------------------------------------------- + +.. automodule:: ytdl_subscribe.validators.strict_dict_validator + :members: + :undoc-members: + :show-inheritance: + +ytdl\_subscribe.validators.string\_datetime module +-------------------------------------------------- + +.. automodule:: ytdl_subscribe.validators.string_datetime + :members: + :undoc-members: + :show-inheritance: + +ytdl\_subscribe.validators.string\_formatter\_validators module +--------------------------------------------------------------- + +.. automodule:: ytdl_subscribe.validators.string_formatter_validators + :members: + :undoc-members: + :show-inheritance: + +ytdl\_subscribe.validators.string\_select\_validator module +----------------------------------------------------------- + +.. automodule:: ytdl_subscribe.validators.string_select_validator + :members: + :undoc-members: + :show-inheritance: + +ytdl\_subscribe.validators.validators module -------------------------------------------- -.. automodule:: ytdl_subscribe.validators.exceptions +.. automodule:: ytdl_subscribe.validators.validators :members: :undoc-members: :show-inheritance: diff --git a/tests/unit/entries/conftest.py b/tests/unit/entries/conftest.py index 9ba72040..e29c3868 100644 --- a/tests/unit/entries/conftest.py +++ b/tests/unit/entries/conftest.py @@ -75,6 +75,7 @@ def mock_entry_to_dict( uid, title, ext, + extractor, upload_date, upload_year, thumbnail_ext, @@ -87,6 +88,7 @@ def mock_entry_to_dict( "title": title, "sanitized_title": title, "ext": ext, + "extractor": extractor, "upload_date": upload_date, "upload_date_standardized": f"{upload_year}-{_pad(upload_month)}-{_pad(upload_day)}", "upload_year": upload_year, @@ -134,15 +136,11 @@ def validate_entry_properties( def _validate_entry_properties(entry: Entry): assert entry.uid == uid assert entry.title == title - # Title does not require sanitizing, so should be the same assert entry.sanitized_title == title assert entry.upload_date == upload_date assert entry.upload_year == upload_year assert entry.ext == ext assert entry.thumbnail_ext == thumbnail_ext - assert entry.download_file_name == download_file_name - assert entry.download_thumbnail_name == download_thumbnail_name - assert entry.playlist_metadata is None assert entry.extractor == extractor return True diff --git a/tests/unit/entries/test_soundcloud_entries.py b/tests/unit/entries/test_soundcloud_entries.py index 9a76d19c..32460f81 100644 --- a/tests/unit/entries/test_soundcloud_entries.py +++ b/tests/unit/entries/test_soundcloud_entries.py @@ -40,7 +40,6 @@ def mock_soundcloud_track_to_dict( "album": title, "sanitized_album": title, "album_year": upload_year, - "is_premiere": is_premiere, } ) @@ -71,7 +70,8 @@ def validate_soundcloud_track_properties( assert soundcloud_track.album == title assert soundcloud_track.sanitized_album == title assert soundcloud_track.album_year == upload_year - assert soundcloud_track.is_premiere == is_premiere + + assert soundcloud_track.is_premiere() == is_premiere return True diff --git a/ytdl_subscribe/entries/base_entry.py b/ytdl_subscribe/entries/base_entry.py new file mode 100644 index 00000000..b0b5188b --- /dev/null +++ b/ytdl_subscribe/entries/base_entry.py @@ -0,0 +1,53 @@ +from abc import ABC +from dataclasses import dataclass +from typing import Any +from typing import Dict +from typing import Optional + + +@dataclass +class PlaylistMetadata: + """ + Metadata for storing playlist information. + """ + + playlist_index: int + playlist_id: str + playlist_extractor: str + + +class BaseEntry(ABC): + """ + Abstract entry object to represent anything download from ytdl (playlist metadata, media, etc). + """ + + def __init__(self, entry_dict: Dict, working_directory: Optional[str] = None): + """ + Initialize the entry using ytdl metadata + + Parameters + ---------- + entry_dict + Entry metadata + working_directory + Optional. Directory that the entry is downloaded to + """ + self._working_directory = working_directory + self._kwargs = entry_dict + + def kwargs_contains(self, key: str) -> bool: + """Returns whether internal kwargs contains the specified key""" + return key in self._kwargs + + def kwargs(self, key) -> Any: + """Returns an internal kwarg value supplied from ytdl""" + if not self.kwargs_contains(key): + raise KeyError(f"Expected '{key}' in {self.__class__.__name__} but does not exist.") + return self._kwargs[key] + + def working_directory(self) -> str: + if self._working_directory is None: + raise ValueError( + "Entry working directory is not set when trying to access its download file path" + ) + return self._working_directory diff --git a/ytdl_subscribe/entries/entry.py b/ytdl_subscribe/entries/entry.py index de82a195..4b47977f 100644 --- a/ytdl_subscribe/entries/entry.py +++ b/ytdl_subscribe/entries/entry.py @@ -1,193 +1,29 @@ -from abc import ABC -from dataclasses import dataclass from pathlib import Path -from typing import Any from typing import Dict -from typing import Optional +from typing import final -from yt_dlp.utils import sanitize_filename +from ytdl_subscribe.entries.base_entry import BaseEntry +from ytdl_subscribe.entries.variables.entry_variables import EntryVariables -@dataclass -class PlaylistMetadata: - """ - Metadata for storing playlist information. - """ - - playlist_index: int - playlist_id: str - playlist_extractor: str - - -class BaseEntry(ABC): - """ - Abstract entry object to represent anything download from ytdl (playlist metadata, media, etc). - """ - - def __init__(self, entry_dict: Dict, working_directory: Optional[str] = None): - """ - Initialize the entry using ytdl metadata - - Parameters - ---------- - entry_dict - Entry metadata - working_directory - Optional. Directory that the entry is downloaded to - """ - self._working_directory = working_directory - self._kwargs = entry_dict - - def kwargs_contains(self, key: str) -> bool: - """Returns whether internal kwargs contains the specified key""" - return key in self._kwargs - - def kwargs(self, key) -> Any: - """Returns an internal kwarg value supplied from ytdl""" - if not self.kwargs_contains(key): - raise KeyError(f"Expected '{key}' in {self.__class__.__name__} but does not exist.") - return self._kwargs[key] - - @property - def working_directory(self) -> str: - if self._working_directory is None: - raise ValueError( - "Entry working directory is not set when trying to access its download file path" - ) - return self._working_directory - - @property - def uid(self) -> str: - """Returns the entry's unique id""" - return self.kwargs("id") - - @property - def extractor(self) -> str: - """ - :return: The ytdl extrator name - """ - return self.kwargs("extractor") - - @property - def playlist_metadata(self) -> Optional[PlaylistMetadata]: - """ - Reserved for any child entry class that resides in a playlist. - - :return: Playlist metadata for this entry - """ - return None - - def to_dict(self) -> Dict[str, str]: - """Returns the entry's values as a dictionary""" - return { - "uid": self.uid, - } - - -class Entry(BaseEntry): +class Entry(EntryVariables, BaseEntry): """ Entry object to represent a single media object returned from yt-dlp. """ - @property - def title(self) -> str: - """Returns the entry's title""" - return self.kwargs("title") - - @property - def sanitized_title(self) -> str: - """Returns the entry's sanitized title""" - return sanitize_filename(self.title) - - @property - def ext(self) -> str: - """Returns the entry's file extension""" - return self.kwargs("ext") - - @property - def upload_date(self) -> str: - """Returns the entry's upload date""" - return self.kwargs("upload_date") - - @property - def upload_year(self) -> int: - """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 upload_date_standardized(self) -> str: - """ - :return: upload date as YYYY-MM-DD - """ - return f"{self.upload_year}-{self.upload_month_padded}-{self.upload_day_padded}" - - @property - def description(self) -> str: - return self.kwargs("description") - - @property - def download_file_name(self) -> str: - """Returns the entry's file name when downloaded locally""" - return f"{self.uid}.{self.ext}" - - @property - def download_file_path(self) -> str: + def get_download_file_path(self) -> str: """Returns the entry's file path to where it was downloaded""" - return str(Path(self.working_directory) / self.download_file_name) + return str(Path(self.working_directory()) / f"{self.uid}.{self.ext}") - @property - def thumbnail_ext(self) -> str: - """ - :return: The entry's thumbnail extension - """ - return self.kwargs("thumbnail").split(".")[-1] - - @property - def download_thumbnail_name(self) -> str: - """Returns the thumbnail's file name when downloaded locally""" - return f"{self.uid}.{self.thumbnail_ext}" - - @property - def download_thumbnail_path(self) -> str: + def get_download_thumbnail_path(self) -> str: """Returns the entry's thumbnail's file path to where it was downloaded""" - return str(Path(self.working_directory) / self.download_thumbnail_name) + return str(Path(self.working_directory()) / f"{self.uid}.{self.thumbnail_ext}") - def to_dict(self) -> Dict: - """Returns the entry's values as a dictionary""" - return dict( - super().to_dict(), - **{ - "title": self.title, - "sanitized_title": self.sanitized_title, - "description": self.description, - "ext": self.ext, - "thumbnail_ext": self.thumbnail_ext, - "upload_date": self.upload_date, - "upload_date_standardized": self.upload_date_standardized, - "upload_year": self.upload_year, - "upload_month": self.upload_month, - "upload_month_padded": self.upload_month_padded, - "upload_day": self.upload_day, - "upload_day_padded": self.upload_day_padded, - }, - ) + @final + def to_dict(self) -> Dict[str, str]: + """ + Returns + ------- + The variables in dictionary format + """ + return self._to_dict() diff --git a/ytdl_subscribe/entries/soundcloud.py b/ytdl_subscribe/entries/soundcloud.py index 6b0b581d..324a05cb 100644 --- a/ytdl_subscribe/entries/soundcloud.py +++ b/ytdl_subscribe/entries/soundcloud.py @@ -1,45 +1,17 @@ from typing import Dict from typing import List -from typing import Optional - -from yt_dlp.utils import sanitize_filename +from ytdl_subscribe.entries.base_entry import PlaylistMetadata from ytdl_subscribe.entries.entry import Entry -from ytdl_subscribe.entries.entry import PlaylistMetadata +from ytdl_subscribe.entries.variables.soundcloud_variables import SoundcloudVariables -class SoundcloudTrack(Entry): +class SoundcloudTrack(SoundcloudVariables, Entry): """ Entry object to represent a Soundcloud track yt-dlp that is a single, which implies it does not belong to an album. """ - @property - def track_number(self) -> int: - """Returns the entry's track number. Since this is a single, it will always be 1""" - return 1 - - @property - def track_number_padded(self) -> str: - """Returns the entry's padded track number, to the tens-place""" - return f"{self.track_number:02d}" - - @property - def album(self) -> str: - """Returns the entry's album name. Since this is a single, set the album to the title""" - return self.title - - @property - def sanitized_album(self) -> str: - """Returns the entry's sanitized album name""" - return sanitize_filename(self.album) - - @property - def album_year(self) -> int: - """Returns the entry's album year. Since this is a single, use the upload year""" - return self.upload_year - - @property def is_premiere(self) -> bool: """ Returns whether the entry is a premier track. Check this by seeing if the track's url is @@ -47,20 +19,6 @@ class SoundcloudTrack(Entry): """ return "/preview/" in self.kwargs("url") - def to_dict(self) -> Dict: - """Returns the entry's values as a dictionary.""" - return dict( - super().to_dict(), - **{ - "track_number": self.track_number, - "track_number_padded": self.track_number_padded, - "album": self.album, - "sanitized_album": self.sanitized_album, - "album_year": self.album_year, - "is_premiere": self.is_premiere, - }, - ) - class SoundcloudAlbumTrack(SoundcloudTrack): """ @@ -83,10 +41,6 @@ class SoundcloudAlbumTrack(SoundcloudTrack): self._album_year = album_year self._playlist_metadata = playlist_metadata - @property - def playlist_metadata(self) -> Optional[PlaylistMetadata]: - return self._playlist_metadata - @property def track_number(self) -> int: """Returns the entry's track number""" @@ -112,7 +66,7 @@ class SoundcloudAlbumTrack(SoundcloudTrack): ) -> "SoundcloudAlbumTrack": return SoundcloudAlbumTrack( entry_dict=soundcloud_track._kwargs, # pylint: disable=protected-access - working_directory=soundcloud_track.working_directory, + working_directory=soundcloud_track.working_directory(), album=album, album_year=album_year, playlist_metadata=playlist_metadata, @@ -143,7 +97,7 @@ class SoundcloudAlbum(Entry): tracks = [ track for track in self._single_tracks - if not (skip_premiere_tracks and track.is_premiere) + if not (skip_premiere_tracks and track.is_premiere()) ] album_tracks = [ diff --git a/ytdl_subscribe/entries/variables/__init__.py b/ytdl_subscribe/entries/variables/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ytdl_subscribe/entries/variables/entry_variables.py b/ytdl_subscribe/entries/variables/entry_variables.py new file mode 100644 index 00000000..5323a141 --- /dev/null +++ b/ytdl_subscribe/entries/variables/entry_variables.py @@ -0,0 +1,156 @@ +from typing import Dict +from typing import final + +from yt_dlp.utils import sanitize_filename + +from ytdl_subscribe.entries.base_entry import BaseEntry + +# This file contains mixins to a BaseEntry subclass. Ignore pylint's "no kwargs member" suggestion +# pylint: disable=no-member + + +class BaseEntryVariables: + """ + Abstract entry object to represent usable variables for formatting presets and subscriptions + """ + + @property + def uid(self: BaseEntry) -> str: + """ + Returns + ------- + The entry's unique ID + """ + return self.kwargs("id") + + @property + def extractor(self: BaseEntry) -> str: + """ + Returns + ------- + The ytdl extractor name + """ + return self.kwargs("extractor") + + @final + def _to_dict(self) -> Dict[str, str]: + """ + Returns + ------- + Dictionary containing all variables + """ + cls = self.__class__ + property_names = [prop for prop in dir(cls) if isinstance(getattr(cls, prop), property)] + + return {property_name: getattr(self, property_name) for property_name in property_names} + + +class EntryVariables(BaseEntryVariables): + @property + def title(self: BaseEntry) -> str: + """ + Returns + ------- + The title of the entry + """ + return self.kwargs("title") + + @property + def sanitized_title(self) -> str: + """ + Returns + ------- + The sanitized title of the entry, which is safe to use for Unix and Windows file names. + """ + return sanitize_filename(self.title) + + @property + def ext(self: BaseEntry) -> str: + """ + Returns + ------- + The downloaded entry's file extension + """ + return self.kwargs("ext") + + @property + def thumbnail_ext(self: BaseEntry) -> str: + """ + Returns + ------- + The download entry's thumbnail extension + """ + # TODO: Always return jpg + return self.kwargs("thumbnail").split(".")[-1] + + @property + def upload_date(self: BaseEntry) -> str: + """ + Returns + ------- + The entry's uploaded date, in YYYYMMDD format. + """ + return self.kwargs("upload_date") + + @property + def upload_year(self) -> int: + """ + 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 to two digits, i.e. March returns "03" + """ + return self.upload_date[4:6] + + @property + def upload_day_padded(self) -> str: + """ + Returns + ------- + The entry's upload day padded to two digits, i.e. the fifth returns "05" + """ + return self.upload_date[6:8] + + @property + def upload_month(self) -> int: + """ + Returns + ------- + The upload month as an integer (no padding). + """ + return int(self.upload_month_padded.lstrip("0")) + + @property + def upload_day(self) -> int: + """ + Returns + ------- + The upload day as an integer (no padding). + """ + return int(self.upload_day_padded.lstrip("0")) + + @property + def upload_date_standardized(self) -> str: + """ + Returns + ------- + The uploaded date formatted as YYYY-MM-DD + """ + return f"{self.upload_year}-{self.upload_month_padded}-{self.upload_day_padded}" + + @property + def description(self: BaseEntry) -> str: + """ + Returns + ------- + The description of the entry. + """ + return self.kwargs("description") diff --git a/ytdl_subscribe/entries/variables/soundcloud_variables.py b/ytdl_subscribe/entries/variables/soundcloud_variables.py new file mode 100644 index 00000000..915297db --- /dev/null +++ b/ytdl_subscribe/entries/variables/soundcloud_variables.py @@ -0,0 +1,54 @@ +from yt_dlp.utils import sanitize_filename + +from ytdl_subscribe.entries.variables.entry_variables import EntryVariables + +# This file contains mixins to a BaseEntry subclass. Ignore pylint's "no kwargs member" suggestion +# pylint: disable=no-member + + +class SoundcloudVariables(EntryVariables): + @property + def track_number(self) -> int: + """ + Returns + ------- + The entry's track number within an album. For singles, it will always be 1. + """ + return 1 + + @property + def track_number_padded(self) -> str: + """ + Returns + ------- + The entry's track number, padded two digits. + """ + return f"{self.track_number:02d}" + + @property + def album(self) -> str: + """ + Returns + ------- + The entry's album name. For singles, it will be the same as the title. + """ + return self.title + + @property + def sanitized_album(self) -> str: + """ + Returns + ------- + The entry's sanitized album name, which is safe to use for Unix and Windows file names. + """ + return sanitize_filename(self.album) + + @property + def album_year(self) -> int: + """ + Returns + ------- + The entry's album year, which is determined by the latest year amongst all tracks in the + album. + """ + return self.upload_year diff --git a/ytdl_subscribe/entries/variables/youtube_variables.py b/ytdl_subscribe/entries/variables/youtube_variables.py new file mode 100644 index 00000000..fd5fcfb7 --- /dev/null +++ b/ytdl_subscribe/entries/variables/youtube_variables.py @@ -0,0 +1,20 @@ +from ytdl_subscribe.entries.base_entry import BaseEntry +from ytdl_subscribe.entries.variables.entry_variables import EntryVariables + +# This file contains mixins to a BaseEntry subclass. Ignore pylint's "no kwargs member" suggestion +# pylint: disable=no-member + + +class YoutubeVideoVariables(EntryVariables): + @property + def track_title(self: BaseEntry) -> str: + """ + Returns + ------- + The track title of a music video if it is available, otherwise it falls back to the title. + """ + # Try to get the track, fall back on title + if self.kwargs_contains("track"): + return self.kwargs("track") + + return super().title diff --git a/ytdl_subscribe/entries/youtube.py b/ytdl_subscribe/entries/youtube.py index 36834af6..9275304e 100644 --- a/ytdl_subscribe/entries/youtube.py +++ b/ytdl_subscribe/entries/youtube.py @@ -2,27 +2,15 @@ import os.path from pathlib import Path from ytdl_subscribe.entries.entry import Entry +from ytdl_subscribe.entries.variables.youtube_variables import YoutubeVideoVariables -class YoutubeVideo(Entry): +class YoutubeVideo(YoutubeVideoVariables, Entry): """ Entry object to represent a Youtube video. """ - @property - def title(self) -> str: - """ - Returns the title of the youtube video. Tries to get the track name if it is available, - otherwise it falls back to the title. - """ - # Try to get the track, fall back on title - if self.kwargs_contains("track"): - return self.kwargs("track") - - return super().title - - @property - def download_thumbnail_path(self) -> str: + def get_download_thumbnail_path(self) -> str: """ Returns ------- @@ -30,7 +18,7 @@ class YoutubeVideo(Entry): thumbnail entry is not the actual thumbnail it downloaded. So check all the possible extensions that it may have downloaded, and see if that thumbnail's extension exists. - TODO: move this into the init so the thumbnail_ext variable can be set accordingly + TODO: always make thumbnails jpg """ thumbnails = self.kwargs("thumbnails") possible_thumbnail_exts = set() @@ -38,8 +26,8 @@ class YoutubeVideo(Entry): possible_thumbnail_exts.add(thumbnail["url"].split(".")[-1]) for ext in possible_thumbnail_exts: - possible_thumbnail_path = str(Path(self.working_directory) / f"{self.uid}.{ext}") + possible_thumbnail_path = str(Path(self.working_directory()) / f"{self.uid}.{ext}") if os.path.isfile(possible_thumbnail_path): return possible_thumbnail_path - return super().download_thumbnail_path + return super().get_download_thumbnail_path() diff --git a/ytdl_subscribe/plugins/convert_thumbnail.py b/ytdl_subscribe/plugins/convert_thumbnail.py index a8fd553c..f7aea6e2 100644 --- a/ytdl_subscribe/plugins/convert_thumbnail.py +++ b/ytdl_subscribe/plugins/convert_thumbnail.py @@ -31,14 +31,14 @@ class ConvertThumbnailPlugin(Plugin[ConvertThumbnailOptions]): Convert the source thumbnail to the desired type. Leave the original file name and extension intact to let output_options deal with renaming it. """ - if not os.path.isfile(entry.download_thumbnail_path): + if not os.path.isfile(entry.get_download_thumbnail_path()): raise ValueError("Thumbnail not found") - image: Image = pil_open(entry.download_thumbnail_path).convert("RGB") + image: Image = pil_open(entry.get_download_thumbnail_path()).convert("RGB") # Pillow likes the formal 'jpeg' name and not 'jpg' thumbnail_format = self.plugin_options.convert_to.value if thumbnail_format == "jpg": thumbnail_format = "jpeg" - image.save(fp=entry.download_thumbnail_path, format=thumbnail_format) + image.save(fp=entry.get_download_thumbnail_path(), format=thumbnail_format) diff --git a/ytdl_subscribe/plugins/music_tags.py b/ytdl_subscribe/plugins/music_tags.py index 2359c509..7321579d 100644 --- a/ytdl_subscribe/plugins/music_tags.py +++ b/ytdl_subscribe/plugins/music_tags.py @@ -28,7 +28,7 @@ class MusicTagsPlugin(Plugin[MusicTagsOptions]): """ Tags the entry's audio file using values defined in the metadata options """ - audio_file = music_tag.load_file(entry.download_file_path) + audio_file = music_tag.load_file(entry.get_download_file_path()) for tag, tag_formatter in self.plugin_options.tags.dict.items(): audio_file[tag] = self.overrides.apply_formatter(formatter=tag_formatter, entry=entry) audio_file.save() diff --git a/ytdl_subscribe/subscriptions/subscription.py b/ytdl_subscribe/subscriptions/subscription.py index 58f6d2c0..a112132b 100644 --- a/ytdl_subscribe/subscriptions/subscription.py +++ b/ytdl_subscribe/subscriptions/subscription.py @@ -108,7 +108,7 @@ class Subscription: ) self._enhanced_download_archive.mapping.add_entry(entry, output_file_name) self._copy_file_to_output_directory( - source_file_path=entry.download_file_path, output_file_name=output_file_name + source_file_path=entry.get_download_file_path(), output_file_name=output_file_name ) if self.output_options.thumbnail_name: @@ -117,7 +117,7 @@ class Subscription: ) self._enhanced_download_archive.mapping.add_entry(entry, output_thumbnail_name) self._copy_file_to_output_directory( - source_file_path=entry.download_thumbnail_path, + source_file_path=entry.get_download_thumbnail_path(), output_file_name=output_thumbnail_name, )