diff --git a/ytdl_subscribe/entries/entry.py b/ytdl_subscribe/entries/entry.py index 87fb1d43..b5c8e0e8 100644 --- a/ytdl_subscribe/entries/entry.py +++ b/ytdl_subscribe/entries/entry.py @@ -12,6 +12,7 @@ class EntryFormatter: """ Ensures user-created formatter strings are valid """ + FIELDS_VALIDATOR = re.compile(r"{([a-z_]+?)}") def __init__(self, format_string: str): @@ -50,14 +51,17 @@ class Entry: """ def __init__(self, **kwargs): + """ + Initialize the entry using ytdl metadata + """ self._kwargs = kwargs def kwargs_contains(self, key: str) -> bool: - """Check if internal kwargs contains the specified key""" + """Returns whether internal kwargs contains the specified key""" return key in self._kwargs def kwargs(self, key) -> Any: - """Get an internal kwarg value supplied from ytdl""" + """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." @@ -66,55 +70,55 @@ class Entry: @property def uid(self) -> str: - """Get the entry's unique id""" + """Returns the entry's unique id""" return self.kwargs("id") @property def title(self) -> str: - """Get the entry's title""" + """Returns the entry's title""" return self.kwargs("title") @property def sanitized_title(self) -> str: - """Get the entry's sanitized title""" + """Returns the entry's sanitized title""" return sanitize(self.title) @property def ext(self) -> str: - """Get the entry's file extension""" + """Returns the entry's file extension""" return self.kwargs("ext") @property def upload_date(self) -> str: - """Get the entry's upload date""" + """Returns the entry's upload date""" return self.kwargs("upload_date") @property def upload_year(self) -> int: - """Get the entry's upload year""" + """Returns the entry's upload year""" return int(self.upload_date[:4]) @property def thumbnail(self) -> str: - """Get the entry's thumbnail url""" + """Returns the entry's thumbnail url""" return self.kwargs("thumbnail") @property def thumbnail_ext(self) -> str: - """Get the entry's thumbnail extension""" + """Returns the entry's thumbnail extension""" return self.thumbnail.split(".")[-1] @property def download_file_name(self) -> str: - """Get the entry's file name when downloaded locally""" + """Returns the entry's file name when downloaded locally""" return f"{self.uid}.{self.ext}" def file_path(self, relative_directory: str): - """Get the entry's file path with respect to the relative directory""" + """Returns the entry's file path with respect to the relative directory""" return str(Path(relative_directory) / self.download_file_name) def to_dict(self) -> Dict: - """Expose the entry's values as a dictionary""" + """Returns the entry's values as a dictionary""" return { "uid": self.uid, "title": self.title, diff --git a/ytdl_subscribe/entries/soundcloud.py b/ytdl_subscribe/entries/soundcloud.py index 2b9b12ea..476cce7f 100644 --- a/ytdl_subscribe/entries/soundcloud.py +++ b/ytdl_subscribe/entries/soundcloud.py @@ -1,6 +1,5 @@ from typing import Dict from typing import List -from typing import Type from sanitize_filename import sanitize @@ -8,33 +7,48 @@ from ytdl_subscribe.entries.entry import Entry class SoundcloudTrack(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(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 + a preview. + """ return "/preview/" in self.kwargs("url") def to_dict(self) -> Dict: + """Returns the entry's values as a dictionary.""" return dict( - super(SoundcloudTrack, self).to_dict(), + super().to_dict(), **{ "track_number": self.track_number, "track_number_padded": self.track_number_padded, @@ -47,43 +61,77 @@ class SoundcloudTrack(Entry): class SoundcloudAlbumTrack(SoundcloudTrack): - def __init__(self, album, track_number, **kwargs): - super(SoundcloudAlbumTrack, self).__init__(**kwargs) + """ + Entry object to represent a Soundcloud track yt-dlp that belongs to an album. + """ + + def __init__(self, album: str, track_number: int, album_year: int, **kwargs): + """ + Initialize the album track using album metadata and ytdl metadata for the specific track. + """ + super().__init__(**kwargs) self._album = album self._track_number = track_number + self._album_year = album_year @property def track_number(self) -> int: + """Returns the entry's track number""" return self._track_number @property def album(self) -> str: - return self._album.title + """Returns the entry's album name, fetched from its internal album""" + return self._album @property def album_year(self) -> int: - return self._album.album_year + """Returns the entry's album year, fetched from its internal album""" + return self._album_year class SoundcloudAlbum(Entry): + """ + Entry object to represent a Soundcloud album. + """ + def __init__(self, skip_premiere_tracks: bool, **kwargs): - super(SoundcloudAlbum, self).__init__(**kwargs) + super().__init__(**kwargs) self.skip_premiere_tracks = skip_premiere_tracks + @property + def _single_tracks(self) -> List[SoundcloudTrack]: + """ + Returns all tracks in the album represented by singles. Use this to fetch any + data needed from the tracks before representing it as an album track. + """ + return [SoundcloudTrack(**entry) for entry in self.kwargs("entries")] + @property def album_tracks(self) -> List[SoundcloudAlbumTrack]: - tracks = [ - SoundcloudAlbumTrack(album=self, track_number=i + 1, **entry) + """ + Returns all tracks in the album represented as album-tracks. They will share the + same album name, have ordered track numbers, and a shared album year. + """ + album_tracks = [ + SoundcloudAlbumTrack( + album=self.title, + track_number=i + 1, + album_year=self.album_year, + **entry, + ) for i, entry in enumerate(self.kwargs("entries")) ] if self.skip_premiere_tracks: - tracks = [t for t in tracks if not t.is_premiere] + album_tracks = [t for t in album_tracks if not t.is_premiere] - return tracks + return album_tracks @property def album_year(self) -> int: - return min([track.upload_year for track in self.album_tracks]) + """Returns the album's year, computed by the max upload year amongst all album tracks""" + return max(track.upload_year for track in self._single_tracks) def contains(self, track: SoundcloudTrack) -> bool: - return any([track.uid == t.uid for t in self.album_tracks]) + """Returns whether the album contains a track""" + return any(track.uid == t.uid for t in self._single_tracks) diff --git a/ytdl_subscribe/entries/youtube.py b/ytdl_subscribe/entries/youtube.py index 15a79498..c4efe197 100644 --- a/ytdl_subscribe/entries/youtube.py +++ b/ytdl_subscribe/entries/youtube.py @@ -2,10 +2,18 @@ from ytdl_subscribe.entries.entry import Entry class YoutubeVideo(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(YoutubeVideo, self).title + return super().title diff --git a/ytdl_subscribe/subscriptions/soundcloud.py b/ytdl_subscribe/subscriptions/soundcloud.py index c32ef3fd..42eebb41 100644 --- a/ytdl_subscribe/subscriptions/soundcloud.py +++ b/ytdl_subscribe/subscriptions/soundcloud.py @@ -1,11 +1,10 @@ from typing import List -from typing import Type import yt_dlp as ytdl -from ytdl_subscribe import SubscriptionSource from ytdl_subscribe.entries.soundcloud import SoundcloudAlbum from ytdl_subscribe.entries.soundcloud import SoundcloudTrack +from ytdl_subscribe.enums import SubscriptionSource from ytdl_subscribe.subscriptions.subscription import Subscription diff --git a/ytdl_subscribe/subscriptions/subscription.py b/ytdl_subscribe/subscriptions/subscription.py index 4caafcc0..4996e0ba 100644 --- a/ytdl_subscribe/subscriptions/subscription.py +++ b/ytdl_subscribe/subscriptions/subscription.py @@ -1,15 +1,14 @@ import os from copy import deepcopy from shutil import copyfile -from typing import Type import dicttoxml import music_tag from PIL import Image from sanitize_filename import sanitize -from ytdl_subscribe import SubscriptionSource from ytdl_subscribe.entries.entry import Entry +from ytdl_subscribe.enums import SubscriptionSource class Subscription(object): diff --git a/ytdl_subscribe/subscriptions/youtube.py b/ytdl_subscribe/subscriptions/youtube.py index dfbe6f3a..55b093b5 100644 --- a/ytdl_subscribe/subscriptions/youtube.py +++ b/ytdl_subscribe/subscriptions/youtube.py @@ -4,8 +4,8 @@ from typing import List import yt_dlp as ytdl -from ytdl_subscribe import SubscriptionSource from ytdl_subscribe.entries.youtube import YoutubeVideo +from ytdl_subscribe.enums import SubscriptionSource from ytdl_subscribe.subscriptions.subscription import Subscription