BROKEN STATE - in middle of refactoring for plugin support
This commit is contained in:
parent
279de00c2b
commit
7a3144a838
17 changed files with 408 additions and 270 deletions
81
config.yaml
81
config.yaml
|
|
@ -12,20 +12,19 @@ presets:
|
||||||
output_options:
|
output_options:
|
||||||
output_directory: "/tmp/{sanitized_artist}"
|
output_directory: "/tmp/{sanitized_artist}"
|
||||||
file_name: "[{album_year}] {sanitized_album}/{track_number_padded} - {sanitized_title}.{ext}"
|
file_name: "[{album_year}] {sanitized_album}/{track_number_padded} - {sanitized_title}.{ext}"
|
||||||
convert_thumbnail: "jpeg"
|
thumbnail_name: "[{album_year}] {sanitized_album}/folder.jpg"
|
||||||
thumbnail_name: "[{album_year}] {sanitized_album}/folder.jpeg"
|
|
||||||
maintain_download_archive: True
|
maintain_download_archive: True
|
||||||
metadata_options:
|
convert_thumbnail:
|
||||||
id3:
|
to: "jpg"
|
||||||
id3_version: "2.4"
|
music_tags:
|
||||||
tags:
|
tags:
|
||||||
artist: "{artist}"
|
artist: "{artist}"
|
||||||
albumartist: "{artist}"
|
albumartist: "{artist}"
|
||||||
title: "{title}"
|
title: "{title}"
|
||||||
album: "{album}"
|
album: "{album}"
|
||||||
tracknumber: "{track_number}"
|
tracknumber: "{track_number}"
|
||||||
year: "{album_year}"
|
year: "{album_year}"
|
||||||
genre: "{genre}"
|
genre: "{genre}"
|
||||||
|
|
||||||
music_videos:
|
music_videos:
|
||||||
youtube:
|
youtube:
|
||||||
|
|
@ -35,16 +34,17 @@ presets:
|
||||||
output_options:
|
output_options:
|
||||||
file_name: "{sanitized_artist} - {sanitized_title}.{ext}"
|
file_name: "{sanitized_artist} - {sanitized_title}.{ext}"
|
||||||
convert_thumbnail: "jpeg"
|
convert_thumbnail: "jpeg"
|
||||||
thumbnail_name: "{sanitized_artist} - {sanitized_title}.jpeg"
|
thumbnail_name: "{sanitized_artist} - {sanitized_title}.jpg"
|
||||||
metadata_options:
|
convert_thumbnail:
|
||||||
nfo:
|
to: "jpg"
|
||||||
nfo_name: "{sanitized_artist} - {sanitized_title}.nfo"
|
nfo_tags:
|
||||||
nfo_root: "musicvideo"
|
nfo_name: "{sanitized_artist} - {sanitized_title}.nfo"
|
||||||
tags:
|
nfo_root: "musicvideo"
|
||||||
artist: "{artist}"
|
tags:
|
||||||
title: "{title}"
|
artist: "{artist}"
|
||||||
album: "Music Videos"
|
title: "{title}"
|
||||||
year: "{upload_year}"
|
album: "Music Videos"
|
||||||
|
year: "{upload_year}"
|
||||||
|
|
||||||
yt_channel:
|
yt_channel:
|
||||||
youtube:
|
youtube:
|
||||||
|
|
@ -54,24 +54,25 @@ presets:
|
||||||
output_options:
|
output_options:
|
||||||
file_name: "{output_file_name}.{ext}"
|
file_name: "{output_file_name}.{ext}"
|
||||||
convert_thumbnail: "jpeg"
|
convert_thumbnail: "jpeg"
|
||||||
thumbnail_name: "{output_file_name}.jpeg"
|
thumbnail_name: "{output_file_name}.jpg"
|
||||||
maintain_download_archive: True
|
maintain_download_archive: True
|
||||||
maintain_stale_file_deletion: True
|
maintain_stale_file_deletion: True
|
||||||
metadata_options:
|
convert_thumbnail:
|
||||||
nfo:
|
to: "jpg"
|
||||||
nfo_name: "{output_file_name}.nfo"
|
nfo_tags:
|
||||||
nfo_root: "episodedetails"
|
nfo_name: "{output_file_name}.nfo"
|
||||||
tags:
|
nfo_root: "episodedetails"
|
||||||
title: "{title}"
|
tags:
|
||||||
season: "{upload_year}"
|
title: "{title}"
|
||||||
episode: "{upload_month}{upload_day_padded}"
|
season: "{upload_year}"
|
||||||
plot: "{description}"
|
episode: "{upload_month}{upload_day_padded}"
|
||||||
year: "{upload_year}"
|
plot: "{description}"
|
||||||
aired: "{upload_date_standardized}"
|
year: "{upload_year}"
|
||||||
output_directory_nfo:
|
aired: "{upload_date_standardized}"
|
||||||
nfo_name: "tvshow.nfo"
|
output_directory_nfo_tags:
|
||||||
nfo_root: "tvshow"
|
nfo_name: "tvshow.nfo"
|
||||||
tags:
|
nfo_root: "tvshow"
|
||||||
title: "{tv_show_name}"
|
tags:
|
||||||
|
title: "{tv_show_name}"
|
||||||
overrides:
|
overrides:
|
||||||
output_file_name: "Season {upload_year}/s{upload_year}.e{upload_month_padded}{upload_day_padded} - {sanitized_title}"
|
output_file_name: "Season {upload_year}/s{upload_year}.e{upload_month_padded}{upload_day_padded} - {sanitized_title}"
|
||||||
21
docs/conf.py
21
docs/conf.py
|
|
@ -12,14 +12,15 @@
|
||||||
#
|
#
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
sys.path.insert(0, os.path.abspath('../'))
|
|
||||||
|
sys.path.insert(0, os.path.abspath("../"))
|
||||||
|
|
||||||
|
|
||||||
# -- Project information -----------------------------------------------------
|
# -- Project information -----------------------------------------------------
|
||||||
|
|
||||||
project = 'ytdl-subscribe'
|
project = "ytdl-subscribe"
|
||||||
copyright = '2022, Jesse Bannon'
|
copyright = "2022, Jesse Bannon"
|
||||||
author = 'Jesse Bannon'
|
author = "Jesse Bannon"
|
||||||
|
|
||||||
|
|
||||||
# -- General configuration ---------------------------------------------------
|
# -- General configuration ---------------------------------------------------
|
||||||
|
|
@ -28,17 +29,17 @@ author = 'Jesse Bannon'
|
||||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||||
# ones.
|
# ones.
|
||||||
extensions = [
|
extensions = [
|
||||||
'sphinx.ext.autodoc',
|
"sphinx.ext.autodoc",
|
||||||
'sphinx.ext.napoleon',
|
"sphinx.ext.napoleon",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
# 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
|
# List of patterns, relative to source directory, that match files and
|
||||||
# directories to ignore when looking for source files.
|
# directories to ignore when looking for source files.
|
||||||
# This pattern also affects html_static_path and html_extra_path.
|
# 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 -------------------------------------------------
|
# -- 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
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
# a list of builtin themes.
|
# 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,
|
# 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,
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
# 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
|
# Do not show full module path in api docs
|
||||||
add_module_names = False
|
add_module_names = False
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,9 @@ class SoundcloudDownloader(Downloader):
|
||||||
artist_albums_url = f"{self.artist_url(artist_name)}/albums"
|
artist_albums_url = f"{self.artist_url(artist_name)}/albums"
|
||||||
|
|
||||||
info = self.extract_info(url=artist_albums_url)
|
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]
|
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"
|
artist_tracks_url = f"{self.artist_url(artist_name)}/tracks"
|
||||||
|
|
||||||
info = self.extract_info(url=artist_tracks_url)
|
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"]
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
from abc import ABC
|
from abc import ABC
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
@ -23,10 +24,18 @@ class BaseEntry(ABC):
|
||||||
Abstract entry object to represent anything download from ytdl (playlist metadata, media, etc).
|
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
|
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
|
self._kwargs = kwargs
|
||||||
|
|
||||||
def kwargs_contains(self, key: str) -> bool:
|
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.")
|
raise KeyError(f"Expected '{key}' in {self.__class__.__name__} but does not exist.")
|
||||||
return self._kwargs[key]
|
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
|
@property
|
||||||
def uid(self) -> str:
|
def uid(self) -> str:
|
||||||
"""Returns the entry's unique id"""
|
"""Returns the entry's unique id"""
|
||||||
|
|
@ -133,6 +150,11 @@ class Entry(BaseEntry):
|
||||||
"""Returns the entry's file name when downloaded locally"""
|
"""Returns the entry's file name when downloaded locally"""
|
||||||
return f"{self.uid}.{self.ext}"
|
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
|
@property
|
||||||
def thumbnail_ext(self) -> str:
|
def thumbnail_ext(self) -> str:
|
||||||
"""
|
"""
|
||||||
|
|
@ -145,6 +167,11 @@ class Entry(BaseEntry):
|
||||||
"""Returns the thumbnail's file name when downloaded locally"""
|
"""Returns the thumbnail's file name when downloaded locally"""
|
||||||
return f"{self.uid}.{self.thumbnail_ext}"
|
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:
|
def to_dict(self) -> Dict:
|
||||||
"""Returns the entry's values as a dictionary"""
|
"""Returns the entry's values as a dictionary"""
|
||||||
return dict(
|
return dict(
|
||||||
|
|
|
||||||
|
|
@ -67,11 +67,18 @@ class SoundcloudAlbumTrack(SoundcloudTrack):
|
||||||
Entry object to represent a Soundcloud track yt-dlp that belongs to an album.
|
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.
|
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 = album
|
||||||
self._album_year = album_year
|
self._album_year = album_year
|
||||||
self._playlist_metadata = playlist_metadata
|
self._playlist_metadata = playlist_metadata
|
||||||
|
|
@ -104,6 +111,7 @@ class SoundcloudAlbumTrack(SoundcloudTrack):
|
||||||
playlist_metadata: PlaylistMetadata,
|
playlist_metadata: PlaylistMetadata,
|
||||||
) -> "SoundcloudAlbumTrack":
|
) -> "SoundcloudAlbumTrack":
|
||||||
return SoundcloudAlbumTrack(
|
return SoundcloudAlbumTrack(
|
||||||
|
working_directory=soundcloud_track.working_directory,
|
||||||
album=album,
|
album=album,
|
||||||
album_year=album_year,
|
album_year=album_year,
|
||||||
playlist_metadata=playlist_metadata,
|
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
|
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.
|
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]:
|
def album_tracks(self, skip_premiere_tracks: bool = True) -> List[SoundcloudAlbumTrack]:
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
0
ytdl_subscribe/plugins/__init__.py
Normal file
0
ytdl_subscribe/plugins/__init__.py
Normal file
41
ytdl_subscribe/plugins/convert_thumbnail.py
Normal file
41
ytdl_subscribe/plugins/convert_thumbnail.py
Normal 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)
|
||||||
32
ytdl_subscribe/plugins/music_tags.py
Normal file
32
ytdl_subscribe/plugins/music_tags.py
Normal 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()
|
||||||
58
ytdl_subscribe/plugins/nfo_tags.py
Normal file
58
ytdl_subscribe/plugins/nfo_tags.py
Normal 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)
|
||||||
59
ytdl_subscribe/plugins/output_directory_nfo_tags.py
Normal file
59
ytdl_subscribe/plugins/output_directory_nfo_tags.py
Normal 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)
|
||||||
73
ytdl_subscribe/plugins/plugin.py
Normal file
73
ytdl_subscribe/plugins/plugin.py
Normal 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
|
||||||
|
|
@ -11,20 +11,11 @@ from typing import Optional
|
||||||
from typing import Type
|
from typing import Type
|
||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
|
|
||||||
import dicttoxml
|
|
||||||
import music_tag
|
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
from ytdl_subscribe.downloaders.downloader import Downloader
|
from ytdl_subscribe.downloaders.downloader import Downloader
|
||||||
from ytdl_subscribe.entries.entry import Entry
|
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 (
|
from ytdl_subscribe.validators.config.config_options.config_options_validator import (
|
||||||
ConfigOptionsValidator,
|
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 (
|
from ytdl_subscribe.validators.config.output_options.output_options_validator import (
|
||||||
OutputOptionsValidator,
|
OutputOptionsValidator,
|
||||||
)
|
)
|
||||||
|
|
@ -94,11 +85,6 @@ class Subscription(Generic[SourceT, EntryT], ABC):
|
||||||
"""Returns the output options defined for this subscription"""
|
"""Returns the output options defined for this subscription"""
|
||||||
return self.__preset_options.output_options
|
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
|
@property
|
||||||
def overrides(self) -> OverridesValidator:
|
def overrides(self) -> OverridesValidator:
|
||||||
"""Returns the overrides defined for this subscription"""
|
"""Returns the overrides defined for this subscription"""
|
||||||
|
|
@ -114,19 +100,7 @@ class Subscription(Generic[SourceT, EntryT], ABC):
|
||||||
"""
|
"""
|
||||||
:return: The formatted output directory
|
:return: The formatted output directory
|
||||||
"""
|
"""
|
||||||
return self._apply_formatter(formatter=self.output_options.output_directory)
|
return self.overrides.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
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_downloader(
|
def get_downloader(
|
||||||
self, downloader_type: Type[DownloaderT], source_ytdl_options: Optional[Dict] = None
|
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,
|
download_archive_file_name=self._enhanced_download_archive.archive_file_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _apply_formatter(
|
def _copy_file_to_output_directory(self, source_file_path: str, output_file_name: str):
|
||||||
self, formatter: StringFormatterValidator, entry: Optional[Entry] = None
|
destination_file_path = Path(self.output_directory) / Path(output_file_name)
|
||||||
) -> str:
|
os.makedirs(os.path.dirname(destination_file_path), exist_ok=True)
|
||||||
"""
|
copyfile(source_file_path, destination_file_path)
|
||||||
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 _post_process_tagging(self, entry: Entry):
|
def _copy_entry_files_to_output_directory(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):
|
|
||||||
# Move the file after all direct file modifications are complete
|
# 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.overrides.apply_formatter(
|
||||||
|
|
||||||
output_file_name = self._apply_formatter(
|
|
||||||
formatter=self.output_options.file_name, entry=entry
|
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)
|
if self.output_options.thumbnail_name:
|
||||||
copyfile(entry_source_file_path, entry_destination_file_path)
|
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)
|
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
|
:param entry: The entry to post-process
|
||||||
"""
|
"""
|
||||||
if self.metadata_options.id3:
|
# before move
|
||||||
self._post_process_tagging(entry)
|
self._copy_entry_files_to_output_directory(entry=entry)
|
||||||
|
|
||||||
self._copy_entry_file_to_output_directory(entry=entry)
|
# after move
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
)
|
|
||||||
|
|
@ -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.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):
|
class MetadataOptionsValidator(StrictDictValidator):
|
||||||
|
|
@ -12,9 +10,9 @@ class MetadataOptionsValidator(StrictDictValidator):
|
||||||
def __init__(self, name, value):
|
def __init__(self, name, value):
|
||||||
super().__init__(name, value)
|
super().__init__(name, value)
|
||||||
|
|
||||||
self.id3 = self._validate_key_if_present(key="id3", validator=Id3Validator)
|
self.id3 = self._validate_key_if_present(key="id3", validator=MusicTagsValidator)
|
||||||
self.nfo = self._validate_key_if_present(key="nfo", validator=NFOValidator)
|
self.nfo = self._validate_key_if_present(key="nfo", validator=NfoTagsValidator)
|
||||||
|
|
||||||
self.output_directory_nfo = self._validate_key_if_present(
|
self.output_directory_nfo = self._validate_key_if_present(
|
||||||
key="output_directory_nfo", validator=OutputDirectoryNFOValidator
|
key="output_directory_nfo", validator=OutputDirectoryNfoTagsValidator
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -3,22 +3,14 @@ from ytdl_subscribe.validators.base.string_formatter_validators import (
|
||||||
OverridesStringFormatterValidator,
|
OverridesStringFormatterValidator,
|
||||||
)
|
)
|
||||||
from ytdl_subscribe.validators.base.string_formatter_validators import StringFormatterValidator
|
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
|
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):
|
class OutputOptionsValidator(StrictDictValidator):
|
||||||
"""Where to output the final files and thumbnails"""
|
"""Where to output the final files and thumbnails"""
|
||||||
|
|
||||||
_required_keys = {"output_directory", "file_name"}
|
_required_keys = {"output_directory", "file_name"}
|
||||||
_optional_keys = {
|
_optional_keys = {
|
||||||
"convert_thumbnail",
|
|
||||||
"thumbnail_name",
|
"thumbnail_name",
|
||||||
"maintain_download_archive",
|
"maintain_download_archive",
|
||||||
"maintain_stale_file_deletion",
|
"maintain_stale_file_deletion",
|
||||||
|
|
@ -37,9 +29,6 @@ class OutputOptionsValidator(StrictDictValidator):
|
||||||
self.file_name: StringFormatterValidator = self._validate_key(
|
self.file_name: StringFormatterValidator = self._validate_key(
|
||||||
key="file_name", validator=StringFormatterValidator
|
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(
|
self.thumbnail_name = self._validate_key_if_present(
|
||||||
key="thumbnail_name", validator=StringFormatterValidator
|
key="thumbnail_name", validator=StringFormatterValidator
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from yt_dlp.utils import sanitize_filename
|
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 DictFormatterValidator
|
||||||
from ytdl_subscribe.validators.base.string_formatter_validators import StringFormatterValidator
|
from ytdl_subscribe.validators.base.string_formatter_validators import StringFormatterValidator
|
||||||
|
|
||||||
|
|
@ -19,3 +22,15 @@ class OverridesValidator(DictFormatterValidator):
|
||||||
name="__should_never_fail__",
|
name="__should_never_fail__",
|
||||||
value=self._value[sanitized_key_name],
|
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)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue