BROKEN STATE - in middle of refactoring for plugin support

This commit is contained in:
jbannon 2022-04-22 06:58:53 +00:00
parent 279de00c2b
commit 7a3144a838
17 changed files with 408 additions and 270 deletions

View file

@ -12,20 +12,19 @@ presets:
output_options:
output_directory: "/tmp/{sanitized_artist}"
file_name: "[{album_year}] {sanitized_album}/{track_number_padded} - {sanitized_title}.{ext}"
convert_thumbnail: "jpeg"
thumbnail_name: "[{album_year}] {sanitized_album}/folder.jpeg"
thumbnail_name: "[{album_year}] {sanitized_album}/folder.jpg"
maintain_download_archive: True
metadata_options:
id3:
id3_version: "2.4"
tags:
artist: "{artist}"
albumartist: "{artist}"
title: "{title}"
album: "{album}"
tracknumber: "{track_number}"
year: "{album_year}"
genre: "{genre}"
convert_thumbnail:
to: "jpg"
music_tags:
tags:
artist: "{artist}"
albumartist: "{artist}"
title: "{title}"
album: "{album}"
tracknumber: "{track_number}"
year: "{album_year}"
genre: "{genre}"
music_videos:
youtube:
@ -35,16 +34,17 @@ presets:
output_options:
file_name: "{sanitized_artist} - {sanitized_title}.{ext}"
convert_thumbnail: "jpeg"
thumbnail_name: "{sanitized_artist} - {sanitized_title}.jpeg"
metadata_options:
nfo:
nfo_name: "{sanitized_artist} - {sanitized_title}.nfo"
nfo_root: "musicvideo"
tags:
artist: "{artist}"
title: "{title}"
album: "Music Videos"
year: "{upload_year}"
thumbnail_name: "{sanitized_artist} - {sanitized_title}.jpg"
convert_thumbnail:
to: "jpg"
nfo_tags:
nfo_name: "{sanitized_artist} - {sanitized_title}.nfo"
nfo_root: "musicvideo"
tags:
artist: "{artist}"
title: "{title}"
album: "Music Videos"
year: "{upload_year}"
yt_channel:
youtube:
@ -54,24 +54,25 @@ presets:
output_options:
file_name: "{output_file_name}.{ext}"
convert_thumbnail: "jpeg"
thumbnail_name: "{output_file_name}.jpeg"
thumbnail_name: "{output_file_name}.jpg"
maintain_download_archive: True
maintain_stale_file_deletion: True
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_date_standardized}"
output_directory_nfo:
nfo_name: "tvshow.nfo"
nfo_root: "tvshow"
tags:
title: "{tv_show_name}"
convert_thumbnail:
to: "jpg"
nfo_tags:
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_date_standardized}"
output_directory_nfo_tags:
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

@ -12,14 +12,15 @@
#
import os
import sys
sys.path.insert(0, os.path.abspath('../'))
sys.path.insert(0, os.path.abspath("../"))
# -- Project information -----------------------------------------------------
project = 'ytdl-subscribe'
copyright = '2022, Jesse Bannon'
author = 'Jesse Bannon'
project = "ytdl-subscribe"
copyright = "2022, Jesse Bannon"
author = "Jesse Bannon"
# -- General configuration ---------------------------------------------------
@ -28,17 +29,17 @@ author = 'Jesse Bannon'
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.napoleon',
"sphinx.ext.autodoc",
"sphinx.ext.napoleon",
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
templates_path = ["_templates"]
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
# -- Options for HTML output -------------------------------------------------
@ -46,12 +47,12 @@ exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'sphinx_rtd_theme'
html_theme = "sphinx_rtd_theme"
# 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".
html_static_path = ['_static']
html_static_path = ["_static"]
# Do not show full module path in api docs
add_module_names = False

View file

@ -31,7 +31,9 @@ class SoundcloudDownloader(Downloader):
artist_albums_url = f"{self.artist_url(artist_name)}/albums"
info = self.extract_info(url=artist_albums_url)
albums = [SoundcloudAlbum(**e) for e in info["entries"]]
albums = [
SoundcloudAlbum(working_direcotry=self.working_directory, **e) for e in info["entries"]
]
return [album for album in albums if album.track_count > 0]
@ -42,4 +44,7 @@ class SoundcloudDownloader(Downloader):
artist_tracks_url = f"{self.artist_url(artist_name)}/tracks"
info = self.extract_info(url=artist_tracks_url)
return [SoundcloudTrack(**e) for e in info["entries"]]
return [
SoundcloudTrack(working_directory=self.working_directory, kwargs=e)
for e in info["entries"]
]

View file

@ -1,5 +1,6 @@
from abc import ABC
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from typing import Dict
from typing import Optional
@ -23,10 +24,18 @@ class BaseEntry(ABC):
Abstract entry object to represent anything download from ytdl (playlist metadata, media, etc).
"""
def __init__(self, **kwargs):
def __init__(self, working_directory: Optional[str] = None, **kwargs):
"""
Initialize the entry using ytdl metadata
Parameters
----------
working_directory
Optional. Directory that the entry is downloaded to
kwargs
Entry metadata
"""
self._working_directory = working_directory
self._kwargs = kwargs
def kwargs_contains(self, key: str) -> bool:
@ -39,6 +48,14 @@ class BaseEntry(ABC):
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"""
@ -133,6 +150,11 @@ class Entry(BaseEntry):
"""Returns the entry's file name when downloaded locally"""
return f"{self.uid}.{self.ext}"
@property
def 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)
@property
def thumbnail_ext(self) -> str:
"""
@ -145,6 +167,11 @@ class Entry(BaseEntry):
"""Returns the thumbnail's file name when downloaded locally"""
return f"{self.uid}.{self.thumbnail_ext}"
@property
def 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)
def to_dict(self) -> Dict:
"""Returns the entry's values as a dictionary"""
return dict(

View file

@ -67,11 +67,18 @@ class SoundcloudAlbumTrack(SoundcloudTrack):
Entry object to represent a Soundcloud track yt-dlp that belongs to an album.
"""
def __init__(self, album: str, album_year: int, playlist_metadata: PlaylistMetadata, **kwargs):
def __init__(
self,
working_directory: str,
album: str,
album_year: int,
playlist_metadata: PlaylistMetadata,
**kwargs,
):
"""
Initialize the album track using album metadata and ytdl metadata for the specific track.
"""
super().__init__(**kwargs)
super().__init__(working_directory=working_directory, **kwargs)
self._album = album
self._album_year = album_year
self._playlist_metadata = playlist_metadata
@ -104,6 +111,7 @@ class SoundcloudAlbumTrack(SoundcloudTrack):
playlist_metadata: PlaylistMetadata,
) -> "SoundcloudAlbumTrack":
return SoundcloudAlbumTrack(
working_directory=soundcloud_track.working_directory,
album=album,
album_year=album_year,
playlist_metadata=playlist_metadata,
@ -122,7 +130,10 @@ class SoundcloudAlbum(Entry):
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")]
return [
SoundcloudTrack(working_directory=self._working_directory, **entry)
for entry in self.kwargs("entries")
]
def album_tracks(self, skip_premiere_tracks: bool = True) -> List[SoundcloudAlbumTrack]:
"""

View file

View file

@ -0,0 +1,41 @@
import os
from PIL.Image import Image
from ytdl_subscribe.entries.entry import Entry
from ytdl_subscribe.plugins.plugin import Plugin
from ytdl_subscribe.plugins.plugin import PluginValidator
from ytdl_subscribe.validators.base.string_select_validator import StringSelectValidator
class ThumbnailTypes(StringSelectValidator):
"""Valid image types that thumbnails can be converted to"""
_select_values = {"jpg"}
class ConvertThumbnailValidator(PluginValidator):
_required_keys = {"to"}
def __init__(self, name, value):
super().__init__(name, value)
self.to = self._validate_key(key="to", validator=ThumbnailTypes)
class ConvertThumbnail(Plugin[ConvertThumbnailValidator]):
def post_process_entry(self, entry: Entry):
"""
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):
raise ValueError("Thumbnail not found")
image = Image.open(entry.download_thumbnail_path).convert("RGB")
# Pillow likes the formal 'jpeg' name and not 'jpg'
thumbnail_format = self.plugin_options.to.value
if thumbnail_format == "jpg":
thumbnail_format = "jpeg"
image.save(fp=entry.download_thumbnail_path, format=thumbnail_format)

View file

@ -0,0 +1,32 @@
import music_tag
from ytdl_subscribe.entries.entry import Entry
from ytdl_subscribe.plugins.plugin import Plugin
from ytdl_subscribe.plugins.plugin import PluginValidator
from ytdl_subscribe.validators.base.string_formatter_validators import DictFormatterValidator
from ytdl_subscribe.validators.base.validators import StringValidator
class MusicTagsValidator(PluginValidator):
_required_keys = {"tags"}
_optional_keys = {"multi_value_separator"}
def __init__(self, name, value):
super().__init__(name, value)
self.tags = self._validate_key(key="tags", validator=DictFormatterValidator)
self.multi_value_separator = self._validate_key_if_present(
key="multi_value_separator", validator=StringValidator
)
class MusicTags(Plugin[MusicTagsValidator]):
def post_process_entry(self, entry: Entry):
"""
Tags the entry's audio file using values defined in the metadata options
"""
audio_file = music_tag.load_file(entry.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()

View file

@ -0,0 +1,58 @@
from pathlib import Path
import dicttoxml
from ytdl_subscribe.entries.entry import Entry
from ytdl_subscribe.plugins.plugin import Plugin
from ytdl_subscribe.plugins.plugin import PluginValidator
from ytdl_subscribe.validators.base.string_formatter_validators import DictFormatterValidator
from ytdl_subscribe.validators.base.string_formatter_validators import StringFormatterValidator
class NfoTagsValidator(PluginValidator):
_required_keys = {"nfo_name", "nfo_root", "tags"}
def __init__(self, name, value):
super().__init__(name, value)
self.nfo_name = self._validate_key(key="nfo_name", validator=StringFormatterValidator)
self.nfo_root = self._validate_key(key="nfo_root", validator=StringFormatterValidator)
self.tags = self._validate_key(key="tags", validator=DictFormatterValidator)
class NfoTags(Plugin[NfoTagsValidator]):
def post_process_entry(self, entry: Entry):
"""
Creates an entry's NFO file using values defined in the metadata options
Parameters
----------
entry: Entry to create an NFO file for
"""
nfo = {}
for tag, tag_formatter in self.plugin_options.tags.dict.items():
nfo[tag] = self.overrides.apply_formatter(formatter=tag_formatter, entry=entry)
# Write the nfo tags to XML with the nfo_root
nfo_root = self.overrides.apply_formatter(
formatter=self.plugin_options.nfo_root, entry=entry
)
xml = dicttoxml.dicttoxml(
obj=nfo,
root=True, # We assume all NFOs have a root. Maybe we should not?
custom_root=nfo_root,
attr_type=False,
)
nfo_file_name = self.overrides.apply_formatter(
formatter=self.plugin_options.nfo_name, entry=entry
)
# Save the nfo's XML to file
nfo_file_path = Path(self.output_directory) / nfo_file_name
with open(nfo_file_path, "wb") as nfo_file:
nfo_file.write(xml)
# Archive the nfo's file name
self.archive_entry_file_name(entry=entry, relative_file_path=nfo_file_name)

View file

@ -0,0 +1,59 @@
from pathlib import Path
import dicttoxml
from ytdl_subscribe.plugins.plugin import Plugin
from ytdl_subscribe.plugins.plugin import PluginValidator
from ytdl_subscribe.validators.base.string_formatter_validators import (
OverridesDictFormatterValidator,
)
from ytdl_subscribe.validators.base.string_formatter_validators import (
OverridesStringFormatterValidator,
)
class OutputDirectoryNfoTagsValidator(PluginValidator):
"""
Validates config values for the output directory nfo tags plugin.
"""
_required_keys = {"nfo_name", "nfo_root", "tags"}
def __init__(self, name, value):
super().__init__(name, value)
# Since this does not create NFOs for entries, we must use the overrides formatter classes
# to ensure we are only using values defined as string literals or in overrides
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=OverridesDictFormatterValidator)
class NfoTags(Plugin[OutputDirectoryNfoTagsValidator]):
def post_process_subscription(self):
"""
Creates an NFO file in the root of the output directory
"""
nfo = {}
for tag, tag_formatter in self.plugin_options.tags.dict.items():
nfo[tag] = self.overrides.apply_formatter(formatter=tag_formatter)
# Write the nfo tags to XML with the nfo_root
nfo_root = self.overrides.apply_formatter(formatter=self.plugin_options.nfo_root)
xml = dicttoxml.dicttoxml(
obj=nfo,
root=True, # We assume all NFOs have a root. Maybe we should not?
custom_root=nfo_root,
attr_type=False,
)
nfo_file_name = self.overrides.apply_formatter(formatter=self.plugin_options.nfo_name)
# Save the nfo's XML to file
nfo_file_path = Path(self.output_directory) / nfo_file_name
with open(nfo_file_path, "wb") as nfo_file:
nfo_file.write(xml)

View file

@ -0,0 +1,73 @@
from abc import ABC
from typing import Generic
from typing import Optional
from typing import TypeVar
from typing import final
from ytdl_subscribe.entries.entry import Entry
from ytdl_subscribe.validators.base.strict_dict_validator import StrictDictValidator
from ytdl_subscribe.validators.base.string_formatter_validators import StringFormatterValidator
from ytdl_subscribe.validators.config.overrides.overrides_validator import OverridesValidator
from ytdl_subscribe.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive
class PluginValidator(StrictDictValidator):
"""
Class that defines the parameters to a plugin
"""
pass
PluginValidatorT = TypeVar("PluginValidatorT", bound=PluginValidator)
class Plugin(Generic[PluginValidatorT], ABC):
"""
Class to define the new plugin functionality
"""
@final
def __init__(
self,
plugin_options: PluginValidatorT,
output_directory: str,
overrides: OverridesValidator,
enhanced_download_archive: EnhancedDownloadArchive,
):
self.plugin_options = plugin_options
self.output_directory = output_directory
self.overrides = overrides
self.__enhanced_download_archive = enhanced_download_archive
@final
def archive_entry_file_name(self, entry: Entry, relative_file_path: str) -> None:
"""
Adds an entry and a file name that belongs to it into the archive mapping.
Parameters
----------
entry:
Optional. The entry the file belongs to
relative_file_path:
The name of the file path relative to the output directory
"""
self.__enhanced_download_archive.mapping.add_entry(
entry=entry, entry_file_path=relative_file_path
)
def post_process_entry(self, entry: Entry):
"""
For each file downloaded, apply post processing to it.
Parameters
----------
entry: Entry to post process
"""
pass
def post_process_subscription(self):
"""
After all downloaded files have been post-processed, apply a subscription-wide post process
"""
pass

View file

@ -11,20 +11,11 @@ from typing import Optional
from typing import Type
from typing import TypeVar
import dicttoxml
import music_tag
from PIL import Image
from ytdl_subscribe.downloaders.downloader import Downloader
from ytdl_subscribe.entries.entry import Entry
from ytdl_subscribe.validators.base.string_formatter_validators import StringFormatterValidator
from ytdl_subscribe.validators.config.config_options.config_options_validator import (
ConfigOptionsValidator,
)
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,
)
@ -94,11 +85,6 @@ class Subscription(Generic[SourceT, EntryT], ABC):
"""Returns the output options defined for this subscription"""
return self.__preset_options.output_options
@property
def metadata_options(self) -> MetadataOptionsValidator:
"""Returns the metadata options defined for this subscription"""
return self.__preset_options.metadata_options
@property
def overrides(self) -> OverridesValidator:
"""Returns the overrides defined for this subscription"""
@ -114,19 +100,7 @@ class Subscription(Generic[SourceT, EntryT], ABC):
"""
: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
)
return self.overrides.apply_formatter(formatter=self.output_options.output_directory)
def get_downloader(
self, downloader_type: Type[DownloaderT], source_ytdl_options: Optional[Dict] = None
@ -143,113 +117,30 @@ class Subscription(Generic[SourceT, EntryT], ABC):
download_archive_file_name=self._enhanced_download_archive.archive_file_name,
)
def _apply_formatter(
self, formatter: StringFormatterValidator, entry: Optional[Entry] = None
) -> str:
"""
Returns the format_string after .format has been called on it using entry (if provided) and
override values
"""
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 _copy_file_to_output_directory(self, source_file_path: str, output_file_name: str):
destination_file_path = Path(self.output_directory) / Path(output_file_name)
os.makedirs(os.path.dirname(destination_file_path), exist_ok=True)
copyfile(source_file_path, destination_file_path)
def _post_process_tagging(self, entry: Entry):
"""
Tags the entry's audio file using values defined in the metadata options
"""
id3_options = self.metadata_options.id3
entry_source_file_path = Path(self.working_directory) / entry.download_file_name
audio_file = music_tag.load_file(entry_source_file_path)
for tag, tag_formatter in id3_options.tags.dict.items():
audio_file[tag] = self._apply_formatter(formatter=tag_formatter, entry=entry)
audio_file.save()
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
:param nfo_options: Options for the NFO
:param entry: Optional. Will pass entry values to nfo string formatters. If None, will only
use override variables that must resolve.
"""
nfo = {}
for tag, tag_formatter in nfo_options.tags.dict.items():
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(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?
custom_root=nfo_root,
attr_type=False,
)
nfo_file_name = self._apply_formatter(formatter=nfo_options.nfo_name, entry=entry)
# Save the nfo's XML to file
nfo_file_path = Path(self.output_directory) / Path(nfo_file_name)
with open(nfo_file_path, "wb") as nfo_file:
nfo_file.write(xml)
# Archive the nfo's file name
self._archive_entry_file_name(entry=entry, relative_file_name=nfo_file_name)
def _post_process_thumbnail(self, entry: Entry):
source_thumbnail_path = Path(self.working_directory) / entry.download_thumbnail_name
# 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_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):
raise ValueError("Hotfix for getting thumbnail file extension failed")
output_thumbnail_name = self._apply_formatter(
formatter=self.output_options.thumbnail_name, entry=entry
)
output_thumbnail_path = Path(self.output_directory) / Path(output_thumbnail_name)
os.makedirs(os.path.dirname(output_thumbnail_path), exist_ok=True)
# If the thumbnail is to be converted, then save the converted thumbnail to the
# output filepath
if self.output_options.convert_thumbnail:
image = Image.open(source_thumbnail_path).convert("RGB")
image.save(
fp=output_thumbnail_path,
format=self.output_options.convert_thumbnail.value,
)
# Otherwise, just copy the downloaded thumbnail
else:
copyfile(source_thumbnail_path, output_thumbnail_path)
# Archive the thumbnail file name
self._archive_entry_file_name(entry=entry, relative_file_name=output_thumbnail_name)
def _copy_entry_file_to_output_directory(self, entry: Entry):
def _copy_entry_files_to_output_directory(self, entry: Entry):
# Move the file after all direct file modifications are complete
entry_source_file_path = Path(self.working_directory) / entry.download_file_name
output_file_name = self._apply_formatter(
output_file_name = self.overrides.apply_formatter(
formatter=self.output_options.file_name, entry=entry
)
entry_destination_file_path = Path(self.output_directory) / Path(output_file_name)
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
)
os.makedirs(os.path.dirname(entry_destination_file_path), exist_ok=True)
copyfile(entry_source_file_path, entry_destination_file_path)
if self.output_options.thumbnail_name:
output_thumbnail_name = self.overrides.apply_formatter(
formatter=self.output_options.thumbnail_name, entry=entry
)
self._enhanced_download_archive.mapping.add_entry(entry, output_thumbnail_name)
self._copy_file_to_output_directory(
source_file_path=entry.download_thumbnail_path,
output_file_name=output_thumbnail_name,
)
self._enhanced_download_archive.mapping.add_entry(entry, output_file_name)
@ -305,16 +196,7 @@ class Subscription(Generic[SourceT, EntryT], ABC):
:param entry: The entry to post-process
"""
if self.metadata_options.id3:
self._post_process_tagging(entry)
# before move
self._copy_entry_files_to_output_directory(entry=entry)
self._copy_entry_file_to_output_directory(entry=entry)
if self.output_options.thumbnail_name:
self._post_process_thumbnail(entry=entry)
if self.metadata_options.nfo:
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)
# after move

View file

@ -1,25 +0,0 @@
from ytdl_subscribe.validators.base.strict_dict_validator import StrictDictValidator
from ytdl_subscribe.validators.base.string_formatter_validators import DictFormatterValidator
from ytdl_subscribe.validators.base.string_select_validator import StringSelectValidator
from ytdl_subscribe.validators.base.validators import StringValidator
class Id3VersionValidator(StringSelectValidator):
_select_values = {"2.3", "2.4"}
class Id3Validator(StrictDictValidator):
_required_keys = {"id3_version", "tags"}
_optional_keys = {"multi_value_separator"}
def __init__(self, name, value):
super().__init__(name, value)
self.id3_version = self._validate_key(
key="id3_version", validator=Id3VersionValidator
).value
self.tags = self._validate_key(key="tags", validator=DictFormatterValidator)
self.multi_value_separator = self._validate_key_if_present(
key="multi_value_separator", validator=StringValidator
)

View file

@ -1,9 +1,7 @@
from ytdl_subscribe.plugins.music_tags import MusicTagsValidator
from ytdl_subscribe.plugins.nfo_tags import NfoTagsValidator
from ytdl_subscribe.plugins.output_directory_nfo_tags import OutputDirectoryNfoTagsValidator
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,9 +10,9 @@ class MetadataOptionsValidator(StrictDictValidator):
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)
self.id3 = self._validate_key_if_present(key="id3", validator=MusicTagsValidator)
self.nfo = self._validate_key_if_present(key="nfo", validator=NfoTagsValidator)
self.output_directory_nfo = self._validate_key_if_present(
key="output_directory_nfo", validator=OutputDirectoryNFOValidator
key="output_directory_nfo", validator=OutputDirectoryNfoTagsValidator
)

View file

@ -1,29 +0,0 @@
from ytdl_subscribe.validators.base.strict_dict_validator import StrictDictValidator
from ytdl_subscribe.validators.base.string_formatter_validators import DictFormatterValidator
from ytdl_subscribe.validators.base.string_formatter_validators import (
OverridesStringFormatterValidator,
)
from ytdl_subscribe.validators.base.string_formatter_validators import StringFormatterValidator
class NFOValidator(StrictDictValidator):
_required_keys = {"nfo_name", "nfo_root", "tags"}
def __init__(self, name, value):
super().__init__(name, value)
self.nfo_name = self._validate_key(key="nfo_name", validator=StringFormatterValidator)
self.nfo_root = self._validate_key(key="nfo_root", validator=StringFormatterValidator)
self.tags = self._validate_key(key="tags", validator=DictFormatterValidator)
class OutputDirectoryNFOValidator(NFOValidator):
def __init__(self, name, value):
super().__init__(name, value)
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)

View file

@ -3,22 +3,14 @@ from ytdl_subscribe.validators.base.string_formatter_validators import (
OverridesStringFormatterValidator,
)
from ytdl_subscribe.validators.base.string_formatter_validators import StringFormatterValidator
from ytdl_subscribe.validators.base.string_select_validator import StringSelectValidator
from ytdl_subscribe.validators.base.validators import BoolValidator
class ConvertThumbnailValidator(StringSelectValidator):
"""Valid image types that thumbnails can be converted to"""
_select_values = {"jpeg"}
class OutputOptionsValidator(StrictDictValidator):
"""Where to output the final files and thumbnails"""
_required_keys = {"output_directory", "file_name"}
_optional_keys = {
"convert_thumbnail",
"thumbnail_name",
"maintain_download_archive",
"maintain_stale_file_deletion",
@ -37,9 +29,6 @@ class OutputOptionsValidator(StrictDictValidator):
self.file_name: StringFormatterValidator = self._validate_key(
key="file_name", validator=StringFormatterValidator
)
self.convert_thumbnail = self._validate_key_if_present(
key="convert_thumbnail", validator=ConvertThumbnailValidator
)
self.thumbnail_name = self._validate_key_if_present(
key="thumbnail_name", validator=StringFormatterValidator
)

View file

@ -1,5 +1,8 @@
from typing import Optional
from yt_dlp.utils import sanitize_filename
from ytdl_subscribe.entries.entry import Entry
from ytdl_subscribe.validators.base.string_formatter_validators import DictFormatterValidator
from ytdl_subscribe.validators.base.string_formatter_validators import StringFormatterValidator
@ -19,3 +22,15 @@ class OverridesValidator(DictFormatterValidator):
name="__should_never_fail__",
value=self._value[sanitized_key_name],
)
def apply_formatter(
self, formatter: StringFormatterValidator, entry: Optional[Entry] = None
) -> str:
"""
Returns the format_string after .format has been called on it using entry (if provided) and
override values
"""
variable_dict = self.dict_with_format_strings
if entry:
variable_dict = dict(entry.to_dict(), **variable_dict)
return formatter.apply_formatter(variable_dict)