pylint spree
This commit is contained in:
parent
1243832178
commit
0545ada1f8
15 changed files with 106 additions and 35 deletions
|
|
@ -1,4 +1,5 @@
|
|||
[MASTER]
|
||||
disable=
|
||||
C0114, # missing-module-docstring
|
||||
R0903, # too-few-public-methods
|
||||
R0903, # too-few-public-methods
|
||||
R0801, # similar lines
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
import tempfile
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
|
|
@ -50,17 +49,14 @@ class Downloader:
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
output_directory: str,
|
||||
working_directory: str,
|
||||
ytdl_options: Optional[Dict] = None,
|
||||
download_archive_file_name: Optional[str] = None,
|
||||
):
|
||||
self.output_directory = output_directory
|
||||
if self.output_directory is None:
|
||||
self.output_directory = tempfile.TemporaryDirectory().name
|
||||
|
||||
self.working_directory = working_directory
|
||||
self.ytdl_options = Downloader._configure_ytdl_options(
|
||||
ytdl_options=ytdl_options,
|
||||
working_directory=self.output_directory,
|
||||
working_directory=self.working_directory,
|
||||
download_archive_file_name=download_archive_file_name,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -62,9 +62,9 @@ class YoutubeDownloader(Downloader):
|
|||
entries: List[YoutubeVideo] = []
|
||||
|
||||
# Load the entries from info.json, ignore the playlist entry
|
||||
for file_name in os.listdir(self.output_directory):
|
||||
for file_name in os.listdir(self.working_directory):
|
||||
if file_name.endswith(".info.json") and not file_name.startswith(playlist_id):
|
||||
with open(Path(self.output_directory) / file_name, "r", encoding="utf-8") as file:
|
||||
with open(Path(self.working_directory) / file_name, "r", encoding="utf-8") as file:
|
||||
entries.append(YoutubeVideo(**json.load(file)))
|
||||
|
||||
return entries
|
||||
|
|
@ -72,8 +72,6 @@ class YoutubeDownloader(Downloader):
|
|||
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))
|
||||
|
||||
|
|
@ -82,9 +80,9 @@ class YoutubeDownloader(Downloader):
|
|||
|
||||
# Load the entries from info.json
|
||||
# TODO dupe code between this and playlist
|
||||
for file_name in os.listdir(self.output_directory):
|
||||
for file_name in os.listdir(self.working_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:
|
||||
with open(Path(self.working_directory) / file_name, "r", encoding="utf-8") as file:
|
||||
entries.append(YoutubeVideo(**json.load(file)))
|
||||
|
||||
return entries
|
||||
|
|
|
|||
|
|
@ -1,24 +1,28 @@
|
|||
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 sanitize_filename import sanitize
|
||||
|
||||
from ytdl_subscribe.validators.base.string_formatter_validators import StringFormatterValidator
|
||||
from ytdl_subscribe.validators.config.overrides.overrides_validator import OverridesValidator
|
||||
|
||||
|
||||
@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, **kwargs):
|
||||
"""
|
||||
Initialize the entry using ytdl metadata
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ class SoundcloudAlbumTrack(SoundcloudTrack):
|
|||
album=album,
|
||||
album_year=album_year,
|
||||
playlist_metadata=playlist_metadata,
|
||||
**soundcloud_track._kwargs,
|
||||
**soundcloud_track._kwargs, # pylint: disable=protected-access
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,10 @@ from ytdl_subscribe.validators.config.source_options.soundcloud_validators impor
|
|||
|
||||
|
||||
class SoundcloudSubscription(Subscription[SourceT], ABC):
|
||||
"""
|
||||
Abstract class for all Soundcloud-based subscriptions. Sets entry type to SoundcloudTrack
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
|
|
@ -33,6 +37,10 @@ class SoundcloudSubscription(Subscription[SourceT], ABC):
|
|||
class SoundcloudAlbumsAndSinglesSubscription(
|
||||
SoundcloudSubscription[SoundcloudAlbumsAndSinglesSourceValidator]
|
||||
):
|
||||
"""
|
||||
Soundcloud subscription to download albums and tracks as singles.
|
||||
"""
|
||||
|
||||
def _extract_info(self) -> List[SoundcloudTrack]:
|
||||
tracks: List[SoundcloudTrack] = []
|
||||
downloader = self.get_downloader(SoundcloudDownloader)
|
||||
|
|
|
|||
|
|
@ -40,6 +40,19 @@ DownloaderT = TypeVar("DownloaderT", bound=Downloader)
|
|||
|
||||
|
||||
class Subscription(Generic[SourceT], ABC):
|
||||
"""
|
||||
Subscription classes are the 'controllers' that perform...
|
||||
|
||||
- Downloading via ytdlp
|
||||
- Adding metadata
|
||||
- Placing files in the output directory
|
||||
|
||||
while configuring each step with provided configs. Child classes are expected to
|
||||
provide SourceValidator (SourceT), which defines the source and its configurable options.
|
||||
In addition, they should provide in the init an Entry type (EntryT), which is the entry that
|
||||
will be returned after downloading.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
|
|
@ -68,6 +81,9 @@ class Subscription(Generic[SourceT], ABC):
|
|||
|
||||
@property
|
||||
def entry_type(self) -> EntryT:
|
||||
"""
|
||||
:return: The Entry type this subscription uses to represent downloaded media
|
||||
"""
|
||||
return self.__entry_type
|
||||
|
||||
@property
|
||||
|
|
@ -96,10 +112,19 @@ class Subscription(Generic[SourceT], ABC):
|
|||
return str(Path(self.__config_options.working_directory.value) / Path(self.name))
|
||||
|
||||
@property
|
||||
def output_directory(self):
|
||||
def output_directory(self) -> str:
|
||||
"""
|
||||
:return: The formatted output directory
|
||||
"""
|
||||
return self._apply_formatter(formatter=self.output_options.output_directory)
|
||||
|
||||
def _archive_entry_file_name(self, entry: Optional[Entry], relative_file_name: str) -> None:
|
||||
"""
|
||||
Adds an entry and a file name that belongs to it into the archive mapping.
|
||||
|
||||
:param entry: Optional. The entry the file belongs to
|
||||
:param relative_file_name: The name of the file
|
||||
"""
|
||||
if entry:
|
||||
self._enhanced_download_archive.mapping.add_entry(
|
||||
entry=entry, entry_file_path=relative_file_name
|
||||
|
|
@ -181,16 +206,18 @@ class Subscription(Generic[SourceT], ABC):
|
|||
|
||||
# Bug that mismatches webp and jpg extensions. Try to hotfix here
|
||||
if not os.path.isfile(source_thumbnail_path):
|
||||
to_replace = f".{entry.thumbnail_ext}"
|
||||
actual_thumbnail_ext = ".webp"
|
||||
if entry.thumbnail_ext == "webp":
|
||||
actual_thumbnail_ext = ".jpg"
|
||||
|
||||
source_thumbnail_path = source_thumbnail_path.replace(
|
||||
f".{entry.thumbnail_ext}", actual_thumbnail_ext
|
||||
source_thumbnail_name = entry.download_thumbnail_name.replace(
|
||||
to_replace, actual_thumbnail_ext
|
||||
)
|
||||
source_thumbnail_path = Path(self.working_directory) / source_thumbnail_name
|
||||
|
||||
if not os.path.isfile(source_thumbnail_path):
|
||||
# TODO: make more formal
|
||||
raise ValueError("Youtube thumbnails are a lie")
|
||||
raise ValueError("Hotfix for getting thumbnail file extension failed")
|
||||
|
||||
output_thumbnail_name = self._apply_formatter(
|
||||
formatter=self.output_options.thumbnail_name, entry=entry
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ from ytdl_subscribe.validators.config.source_options.youtube_validators import (
|
|||
|
||||
|
||||
class YoutubeSubscription(Subscription[SourceT], ABC):
|
||||
"""
|
||||
Abstract class for all Youtube-based subscriptions. Sets entry type to YoutubeVideo
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
|
|
@ -36,6 +40,10 @@ class YoutubeSubscription(Subscription[SourceT], ABC):
|
|||
|
||||
|
||||
class YoutubePlaylistSubscription(YoutubeSubscription[YoutubePlaylistSourceValidator]):
|
||||
"""
|
||||
Youtube subscription to download videos from a playlist
|
||||
"""
|
||||
|
||||
def _extract_info(self) -> List[YoutubeVideo]:
|
||||
return self.get_downloader(YoutubeDownloader).download_playlist(
|
||||
playlist_id=self.source_options.playlist_id.value
|
||||
|
|
@ -43,6 +51,10 @@ class YoutubePlaylistSubscription(YoutubeSubscription[YoutubePlaylistSourceValid
|
|||
|
||||
|
||||
class YoutubeChannelSubscription(YoutubeSubscription[YoutubeChannelSourceValidator]):
|
||||
"""
|
||||
Youtube subscription to download videos from a channel
|
||||
"""
|
||||
|
||||
def _extract_info(self) -> List[YoutubeVideo]:
|
||||
source_ytdl_options = {}
|
||||
source_date_range = self.source_options.get_date_range()
|
||||
|
|
@ -54,6 +66,10 @@ class YoutubeChannelSubscription(YoutubeSubscription[YoutubeChannelSourceValidat
|
|||
|
||||
|
||||
class YoutubeVideoSubscription(YoutubeSubscription[YoutubeVideoSourceValidator]):
|
||||
"""
|
||||
Youtube subscription to download a single video
|
||||
"""
|
||||
|
||||
def _extract_info(self) -> List[YoutubeVideo]:
|
||||
entry = self.get_downloader(YoutubeDownloader).download_video(
|
||||
video_id=self.source_options.video_id.value
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ class StringDatetimeValidator(Validator):
|
|||
|
||||
try:
|
||||
_ = datetime_from_str(self._value)
|
||||
except Exception as e:
|
||||
raise ValidationException(e)
|
||||
except Exception as exc:
|
||||
raise ValidationException(exc) from exc
|
||||
|
||||
@property
|
||||
def datetime_str(self) -> str:
|
||||
|
|
|
|||
|
|
@ -131,11 +131,13 @@ class DictFormatterValidator(LiteralDictValidator):
|
|||
Validates a dictionary made up of key: string_formatters
|
||||
"""
|
||||
|
||||
_key_validator = StringFormatterValidator
|
||||
|
||||
def __init__(self, name, value):
|
||||
super().__init__(name, value)
|
||||
|
||||
for key in self._keys:
|
||||
self._value[key] = self._validate_key(key=key, validator=StringFormatterValidator)
|
||||
self._value[key] = self._validate_key(key=key, validator=self._key_validator)
|
||||
|
||||
@property
|
||||
def dict(self) -> Dict[str, StringFormatterValidator]:
|
||||
|
|
@ -146,3 +148,12 @@ class DictFormatterValidator(LiteralDictValidator):
|
|||
def dict_with_format_strings(self) -> Dict[str, str]:
|
||||
"""Returns dict with the format strings themselves"""
|
||||
return {key: string_formatter.format_string for key, string_formatter in self.dict.items()}
|
||||
|
||||
|
||||
class OverridesDictFormatterValidator(DictFormatterValidator):
|
||||
"""
|
||||
Validates a dictionary made up of key: string_formatters, that must be resolved by overrides
|
||||
only.
|
||||
"""
|
||||
|
||||
_key_validator = OverridesStringFormatterValidator
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
from ytdl_subscribe.validators.base.strict_dict_validator import StrictDictValidator
|
||||
from ytdl_subscribe.validators.config.metadata_options.id3_validator import Id3Validator
|
||||
from ytdl_subscribe.validators.config.metadata_options.nfo_validator import NFOValidator
|
||||
from ytdl_subscribe.validators.config.metadata_options.nfo_validator import (
|
||||
OutputDirectoryNFOValidator,
|
||||
)
|
||||
|
||||
|
||||
class MetadataOptionsValidator(StrictDictValidator):
|
||||
|
|
@ -12,7 +15,6 @@ class MetadataOptionsValidator(StrictDictValidator):
|
|||
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
|
||||
key="output_directory_nfo", validator=OutputDirectoryNFOValidator
|
||||
)
|
||||
|
|
|
|||
|
|
@ -23,3 +23,7 @@ class OutputDirectoryNFOValidator(NFOValidator):
|
|||
self.nfo_name = self._validate_key(
|
||||
key="nfo_name", validator=OverridesStringFormatterValidator
|
||||
)
|
||||
self.nfo_root = self._validate_key(
|
||||
key="nfo_root", validator=OverridesStringFormatterValidator
|
||||
)
|
||||
self.tags = self._validate_key(key="tags", validator=DictFormatterValidator)
|
||||
|
|
|
|||
|
|
@ -110,9 +110,13 @@ class PresetValidator(StrictDictValidator):
|
|||
if validator_dict is None:
|
||||
validator_dict = self._validator_dict
|
||||
|
||||
for name, validator in validator_dict.items():
|
||||
for validator in validator_dict.values():
|
||||
if isinstance(validator, DictValidator):
|
||||
# Usage of protected variables in other validators is fine. The reason to keep them
|
||||
# protected is for readability when using them in subscriptions.
|
||||
# pylint: disable=protected-access
|
||||
self.__recursive_preset_validate(validator._validator_dict)
|
||||
# pylint: enable=protected-access
|
||||
|
||||
if isinstance(validator, OverridesStringFormatterValidator):
|
||||
self.__validate_override_string_formatter_validator(validator)
|
||||
|
|
|
|||
|
|
@ -3,5 +3,3 @@ from ytdl_subscribe.validators.base.validators import LiteralDictValidator
|
|||
|
||||
class YTDLOptionsValidator(LiteralDictValidator):
|
||||
"""Ensures `ytdl_options` is a dict"""
|
||||
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -66,8 +66,8 @@ class DownloadArchive:
|
|||
|
||||
@classmethod
|
||||
def from_file(cls, file_path: str) -> "DownloadArchive":
|
||||
lines = open(file_path, "r", encoding="utf8").readlines()
|
||||
return cls.from_lines(lines=lines)
|
||||
with open(file_path, "r", encoding="utf8") as file:
|
||||
return cls.from_lines(lines=file.readlines())
|
||||
|
||||
def to_file(self, file_path: str) -> "DownloadArchive":
|
||||
with open(file_path, "w", encoding="utf8") as file:
|
||||
|
|
@ -93,7 +93,9 @@ class DownloadMappings:
|
|||
|
||||
@classmethod
|
||||
def from_file(cls, json_file_path: str) -> "DownloadMappings":
|
||||
entry_mappings_json = json.load(open(json_file_path, "r", encoding="utf8"))
|
||||
with open(json_file_path, "r", encoding="utf8") as json_file:
|
||||
entry_mappings_json = json.load(json_file)
|
||||
|
||||
for uid in entry_mappings_json.keys():
|
||||
entry_mappings_json[uid] = DownloadMapping.from_dict(
|
||||
mapping_dict=entry_mappings_json[uid]
|
||||
|
|
|
|||
Loading…
Reference in a new issue