[BACKEND] Remove old format for video_tags, music_tags plugin
This commit is contained in:
parent
e3158583ca
commit
b64a58f467
6 changed files with 12 additions and 231 deletions
|
|
@ -1,6 +1,4 @@
|
||||||
import copy
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from typing import Any
|
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
|
|
@ -14,10 +12,8 @@ from ytdl_sub.entries.script.variable_definitions import VariableDefinitions
|
||||||
from ytdl_sub.utils.file_handler import FileMetadata
|
from ytdl_sub.utils.file_handler import FileMetadata
|
||||||
from ytdl_sub.utils.logger import Logger
|
from ytdl_sub.utils.logger import Logger
|
||||||
from ytdl_sub.validators.audo_codec_validator import AUDIO_CODEC_EXTS
|
from ytdl_sub.validators.audo_codec_validator import AUDIO_CODEC_EXTS
|
||||||
from ytdl_sub.validators.strict_dict_validator import StrictDictValidator
|
|
||||||
from ytdl_sub.validators.string_formatter_validators import ListFormatterValidator
|
from ytdl_sub.validators.string_formatter_validators import ListFormatterValidator
|
||||||
from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator
|
from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator
|
||||||
from ytdl_sub.validators.validators import BoolValidator
|
|
||||||
|
|
||||||
v: VariableDefinitions = VARIABLES
|
v: VariableDefinitions = VARIABLES
|
||||||
|
|
||||||
|
|
@ -40,31 +36,6 @@ def _is_multi_field(tag_name: str) -> bool:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class MusicTagsValidator(StrictDictValidator):
|
|
||||||
"""
|
|
||||||
Validator for the music_tag's `tags` field. Treat each value as a list.
|
|
||||||
Can still specify it like a single value but under-the-hood it's a list of a single element.
|
|
||||||
"""
|
|
||||||
|
|
||||||
_optional_keys = set(list(mediafile.MediaFile.sorted_fields()))
|
|
||||||
|
|
||||||
def __init__(self, name, value):
|
|
||||||
super().__init__(name, value)
|
|
||||||
|
|
||||||
self._tags: Dict[str, List[StringFormatterValidator]] = {}
|
|
||||||
for key in self._keys:
|
|
||||||
self._tags[key] = self._validate_key(key=key, validator=ListFormatterValidator).list
|
|
||||||
|
|
||||||
@property
|
|
||||||
def as_lists(self) -> Dict[str, List[StringFormatterValidator]]:
|
|
||||||
"""
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
Tag formatter(s) as a list
|
|
||||||
"""
|
|
||||||
return self._tags
|
|
||||||
|
|
||||||
|
|
||||||
class MusicTagsOptions(OptionsDictValidator):
|
class MusicTagsOptions(OptionsDictValidator):
|
||||||
"""
|
"""
|
||||||
Adds tags to every download audio file using
|
Adds tags to every download audio file using
|
||||||
|
|
@ -93,40 +64,24 @@ class MusicTagsOptions(OptionsDictValidator):
|
||||||
- "ytdl-sub"
|
- "ytdl-sub"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_optional_keys = {"tags", "embed_thumbnail"}
|
_optional_keys = set(list(mediafile.MediaFile.sorted_fields()))
|
||||||
_allow_extra_keys = True
|
|
||||||
|
|
||||||
def __init__(self, name, value):
|
def __init__(self, name, value):
|
||||||
super().__init__(name, value)
|
super().__init__(name, value)
|
||||||
|
|
||||||
self._embed_thumbnail = self._validate_key_if_present(
|
self._tags: Dict[str, List[StringFormatterValidator]] = {}
|
||||||
key="embed_thumbnail", validator=BoolValidator
|
for key in self._keys:
|
||||||
)
|
self._tags[key] = self._validate_key(key=key, validator=ListFormatterValidator).list
|
||||||
|
|
||||||
new_tags_dict: Dict[str, Any] = copy.deepcopy(value)
|
|
||||||
old_tags_dict = new_tags_dict.pop("tags", {})
|
|
||||||
new_tags_dict.pop("embed_thumbnail", None)
|
|
||||||
|
|
||||||
self._is_old_format = len(old_tags_dict) > 0 or self._embed_thumbnail is not None
|
|
||||||
self._tags = MusicTagsValidator(name=name, value=dict(old_tags_dict, **new_tags_dict))
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def tags(self) -> MusicTagsValidator:
|
def as_lists(self) -> Dict[str, List[StringFormatterValidator]]:
|
||||||
"""
|
"""
|
||||||
Key, values of tag names, tag values. Supports source and override variables.
|
Returns
|
||||||
Supports lists which will get written to MP3s as id3v2.4 multi-tags.
|
-------
|
||||||
|
Tag formatter(s) as a list
|
||||||
"""
|
"""
|
||||||
return self._tags
|
return self._tags
|
||||||
|
|
||||||
@property
|
|
||||||
def embed_thumbnail(self) -> bool:
|
|
||||||
"""
|
|
||||||
Optional. Whether to embed the thumbnail into the audio file.
|
|
||||||
"""
|
|
||||||
if self._embed_thumbnail is None:
|
|
||||||
return False
|
|
||||||
return self._embed_thumbnail.value
|
|
||||||
|
|
||||||
|
|
||||||
class MusicTagsPlugin(Plugin[MusicTagsOptions]):
|
class MusicTagsPlugin(Plugin[MusicTagsOptions]):
|
||||||
plugin_options_type = MusicTagsOptions
|
plugin_options_type = MusicTagsOptions
|
||||||
|
|
@ -142,23 +97,9 @@ class MusicTagsPlugin(Plugin[MusicTagsOptions]):
|
||||||
f"to audio using the audio_extract plugin."
|
f"to audio using the audio_extract plugin."
|
||||||
)
|
)
|
||||||
|
|
||||||
# pylint: disable=protected-access
|
|
||||||
if self.plugin_options._is_old_format:
|
|
||||||
logger.warning(
|
|
||||||
"music_tags.tags is now deprecated. Place your tags directly under music_tags "
|
|
||||||
"instead. The old format will be removed in October of 2023. See "
|
|
||||||
"https://ytdl-sub.readthedocs.io/en/latest/deprecation_notices.html#music-tags "
|
|
||||||
"for more details."
|
|
||||||
)
|
|
||||||
if self.plugin_options.embed_thumbnail:
|
|
||||||
logger.warning(
|
|
||||||
"music_tags.embed_thumbnail is also deprecated. Use the dedicated "
|
|
||||||
"embed_thumbnail plugin instead. This will be removed in October of 2023."
|
|
||||||
)
|
|
||||||
|
|
||||||
# Resolve the tags into this dict
|
# Resolve the tags into this dict
|
||||||
tags_to_write: Dict[str, List[str]] = defaultdict(list)
|
tags_to_write: Dict[str, List[str]] = defaultdict(list)
|
||||||
for tag_name, tag_formatters in self.plugin_options.tags.as_lists.items():
|
for tag_name, tag_formatters in self.plugin_options.as_lists.items():
|
||||||
for tag_formatter in tag_formatters:
|
for tag_formatter in tag_formatters:
|
||||||
tag_value = self.overrides.apply_formatter(formatter=tag_formatter, entry=entry)
|
tag_value = self.overrides.apply_formatter(formatter=tag_formatter, entry=entry)
|
||||||
tags_to_write[tag_name].append(tag_value)
|
tags_to_write[tag_name].append(tag_value)
|
||||||
|
|
@ -180,19 +121,10 @@ class MusicTagsPlugin(Plugin[MusicTagsOptions]):
|
||||||
)
|
)
|
||||||
setattr(audio_file, tag_name, tag_value[0])
|
setattr(audio_file, tag_name, tag_value[0])
|
||||||
|
|
||||||
if self.plugin_options.embed_thumbnail and entry.is_thumbnail_downloaded():
|
|
||||||
with open(entry.get_download_thumbnail_path(), "rb") as thumb:
|
|
||||||
mediafile_img = mediafile.Image(
|
|
||||||
data=thumb.read(), desc="cover", type=mediafile.ImageType.front
|
|
||||||
)
|
|
||||||
|
|
||||||
audio_file.images = [mediafile_img]
|
|
||||||
|
|
||||||
audio_file.save()
|
audio_file.save()
|
||||||
|
|
||||||
# report the tags written
|
# report the tags written
|
||||||
title = f"{'Embedded Thumbnail, ' if self.plugin_options.embed_thumbnail else ''}Music Tags"
|
|
||||||
return FileMetadata.from_dict(
|
return FileMetadata.from_dict(
|
||||||
|
title="Music Tags",
|
||||||
value_dict=tags_to_write,
|
value_dict=tags_to_write,
|
||||||
title=title,
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
import copy
|
|
||||||
from typing import Any
|
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
from ytdl_sub.config.plugin.plugin import Plugin
|
from ytdl_sub.config.plugin.plugin import Plugin
|
||||||
from ytdl_sub.config.validators.options import ToggleableOptionsDictValidator
|
|
||||||
from ytdl_sub.entries.entry import Entry
|
from ytdl_sub.entries.entry import Entry
|
||||||
from ytdl_sub.utils.ffmpeg import add_ffmpeg_metadata_key_values
|
from ytdl_sub.utils.ffmpeg import add_ffmpeg_metadata_key_values
|
||||||
from ytdl_sub.utils.file_handler import FileMetadata
|
from ytdl_sub.utils.file_handler import FileMetadata
|
||||||
|
|
@ -13,7 +10,7 @@ from ytdl_sub.validators.string_formatter_validators import DictFormatterValidat
|
||||||
logger = Logger.get("video-tags")
|
logger = Logger.get("video-tags")
|
||||||
|
|
||||||
|
|
||||||
class VideoTagsOptions(ToggleableOptionsDictValidator):
|
class VideoTagsOptions(DictFormatterValidator):
|
||||||
"""
|
"""
|
||||||
Adds tags to every downloaded video file using ffmpeg ``-metadata key=value`` args.
|
Adds tags to every downloaded video file using ffmpeg ``-metadata key=value`` args.
|
||||||
|
|
||||||
|
|
@ -27,34 +24,6 @@ class VideoTagsOptions(ToggleableOptionsDictValidator):
|
||||||
description: "{description}"
|
description: "{description}"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_optional_keys = {"enable", "tags"}
|
|
||||||
_allow_extra_keys = True
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def partial_validate(cls, name: str, value: Any) -> None:
|
|
||||||
"""
|
|
||||||
Partially validate video tags
|
|
||||||
"""
|
|
||||||
if isinstance(value, dict):
|
|
||||||
value["tags"] = value.get("tags", {})
|
|
||||||
_ = cls(name, value)
|
|
||||||
|
|
||||||
def __init__(self, name, value):
|
|
||||||
super().__init__(name, value)
|
|
||||||
|
|
||||||
new_tags_dict: Dict[str, Any] = copy.deepcopy(value)
|
|
||||||
old_tags_dict = new_tags_dict.pop("tags", {})
|
|
||||||
|
|
||||||
self._is_old_format = len(old_tags_dict) > 0
|
|
||||||
self._tags = DictFormatterValidator(name=name, value=dict(old_tags_dict, **new_tags_dict))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def tags(self) -> DictFormatterValidator:
|
|
||||||
"""
|
|
||||||
Key/values of tag names/values. Supports source and override variables.
|
|
||||||
"""
|
|
||||||
return self._tags
|
|
||||||
|
|
||||||
|
|
||||||
class VideoTagsPlugin(Plugin[VideoTagsOptions]):
|
class VideoTagsPlugin(Plugin[VideoTagsOptions]):
|
||||||
plugin_options_type = VideoTagsOptions
|
plugin_options_type = VideoTagsOptions
|
||||||
|
|
@ -63,17 +32,8 @@ class VideoTagsPlugin(Plugin[VideoTagsOptions]):
|
||||||
"""
|
"""
|
||||||
Tags the entry's audio file using values defined in the metadata options
|
Tags the entry's audio file using values defined in the metadata options
|
||||||
"""
|
"""
|
||||||
# pylint: disable=protected-access
|
|
||||||
if self.plugin_options._is_old_format:
|
|
||||||
logger.warning(
|
|
||||||
"video_tags.tags is now deprecated. Place your tags directly under video_tags "
|
|
||||||
"instead. The old format will be removed in October of 2023. See "
|
|
||||||
"https://ytdl-sub.readthedocs.io/en/latest/deprecation_notices.html#video-tags "
|
|
||||||
"for more details."
|
|
||||||
)
|
|
||||||
|
|
||||||
tags_to_write: Dict[str, str] = {}
|
tags_to_write: Dict[str, str] = {}
|
||||||
for tag_name, tag_formatter in self.plugin_options.tags.dict.items():
|
for tag_name, tag_formatter in self.plugin_options.dict.items():
|
||||||
tag_value = self.overrides.apply_formatter(formatter=tag_formatter, entry=entry)
|
tag_value = self.overrides.apply_formatter(formatter=tag_formatter, entry=entry)
|
||||||
tags_to_write[tag_name] = tag_value
|
tags_to_write[tag_name] = tag_value
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,25 +5,6 @@ from expected_transaction_log import assert_transaction_log_matches
|
||||||
from ytdl_sub.subscriptions.subscription import Subscription
|
from ytdl_sub.subscriptions.subscription import Subscription
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def single_preset_dict_old_format(output_directory):
|
|
||||||
return {
|
|
||||||
"preset": "Single",
|
|
||||||
# test multi-tags
|
|
||||||
"music_tags": {"embed_thumbnail": True, "tags": {"genres": ["multi_tag_1", "multi_tag_2"]}},
|
|
||||||
"format": "worst[ext=mp4]",
|
|
||||||
"audio_extract": {"codec": "mp3", "quality": 320},
|
|
||||||
"ytdl_options": {
|
|
||||||
"postprocessor_args": {"ffmpeg": ["-bitexact"]}, # Must add this for reproducibility
|
|
||||||
},
|
|
||||||
"overrides": {
|
|
||||||
"track_artist": "YouTube",
|
|
||||||
"url": "https://www.youtube.com/watch?v=2lAe1cqCOXo",
|
|
||||||
"music_directory": output_directory,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def single_preset_dict(output_directory):
|
def single_preset_dict(output_directory):
|
||||||
return {
|
return {
|
||||||
|
|
@ -68,32 +49,6 @@ def youtube_release_preset_dict(output_directory):
|
||||||
|
|
||||||
|
|
||||||
class TestAudioExtract:
|
class TestAudioExtract:
|
||||||
@pytest.mark.parametrize("dry_run", [True, False])
|
|
||||||
def test_audio_extract_single_song_old_format(
|
|
||||||
self,
|
|
||||||
default_config,
|
|
||||||
single_preset_dict_old_format,
|
|
||||||
output_directory,
|
|
||||||
dry_run,
|
|
||||||
):
|
|
||||||
subscription = Subscription.from_dict(
|
|
||||||
config=default_config,
|
|
||||||
preset_name="single_song_test",
|
|
||||||
preset_dict=single_preset_dict_old_format,
|
|
||||||
)
|
|
||||||
|
|
||||||
transaction_log = subscription.download(dry_run=dry_run)
|
|
||||||
assert_transaction_log_matches(
|
|
||||||
output_directory=output_directory,
|
|
||||||
transaction_log=transaction_log,
|
|
||||||
transaction_log_summary_file_name="plugins/test_audio_extract_single_old_format.txt",
|
|
||||||
)
|
|
||||||
assert_expected_downloads(
|
|
||||||
output_directory=output_directory,
|
|
||||||
dry_run=dry_run,
|
|
||||||
expected_download_summary_file_name="plugins/test_audio_extract_single_old_format.json",
|
|
||||||
)
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("dry_run", [False])
|
@pytest.mark.parametrize("dry_run", [False])
|
||||||
def test_audio_extract_single_song(
|
def test_audio_extract_single_song(
|
||||||
self,
|
self,
|
||||||
|
|
|
||||||
|
|
@ -14,30 +14,6 @@ from ytdl_sub.utils.system import IS_WINDOWS
|
||||||
from ytdl_sub.utils.thumbnail import try_convert_download_thumbnail
|
from ytdl_sub.utils.thumbnail import try_convert_download_thumbnail
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def single_video_preset_dict_old_video_tags_format(output_directory):
|
|
||||||
return {
|
|
||||||
"preset": "Jellyfin Music Videos",
|
|
||||||
"download": "https://youtube.com/watch?v=HKTNxEqsN3Q",
|
|
||||||
# override the output directory with our fixture-generated dir
|
|
||||||
"output_options": {
|
|
||||||
"maintain_download_archive": False,
|
|
||||||
},
|
|
||||||
"embed_thumbnail": True, # embed thumb into the video
|
|
||||||
"format": "worst[ext=mp4]", # download the worst format so it is fast
|
|
||||||
# also test video tags
|
|
||||||
"video_tags": {
|
|
||||||
"tags": {
|
|
||||||
"title": "{title}",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"overrides": {
|
|
||||||
"music_video_artist": "JMC",
|
|
||||||
"music_video_directory": output_directory,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def single_video_preset_dict(output_directory):
|
def single_video_preset_dict(output_directory):
|
||||||
return {
|
return {
|
||||||
|
|
@ -100,25 +76,6 @@ def single_video_preset_dict_dl_args(single_video_preset_dict):
|
||||||
|
|
||||||
|
|
||||||
class TestYoutubeVideo:
|
class TestYoutubeVideo:
|
||||||
def test_single_video_old_video_tags_format_download(
|
|
||||||
self,
|
|
||||||
default_config,
|
|
||||||
single_video_preset_dict_old_video_tags_format,
|
|
||||||
output_directory,
|
|
||||||
):
|
|
||||||
single_video_subscription = Subscription.from_dict(
|
|
||||||
config=default_config,
|
|
||||||
preset_name="music_video_single_video_test",
|
|
||||||
preset_dict=single_video_preset_dict_old_video_tags_format,
|
|
||||||
)
|
|
||||||
|
|
||||||
transaction_log = single_video_subscription.download(dry_run=True)
|
|
||||||
assert_transaction_log_matches(
|
|
||||||
output_directory=output_directory,
|
|
||||||
transaction_log=transaction_log,
|
|
||||||
transaction_log_summary_file_name="youtube/test_video.txt",
|
|
||||||
)
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("dry_run", [True, False])
|
@pytest.mark.parametrize("dry_run", [True, False])
|
||||||
def test_single_video_download(
|
def test_single_video_download(
|
||||||
self,
|
self,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
".ytdl-sub-single_song_test-download-archive.json": "c8ff22ec3304c9f8dab18cedaed4e8b4",
|
|
||||||
"YouTube/[2019] YouTube Rewind 2019: For the Record | #YouTubeRewind/01 - YouTube Rewind 2019: For the Record | #YouTubeRewind.mp3": "11376667a11bb71565b520f8ce5fa303",
|
|
||||||
"YouTube/[2019] YouTube Rewind 2019: For the Record | #YouTubeRewind/folder.jpg": "50ee47c80f679029f5d3503bb91b045a"
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
Files created:
|
|
||||||
----------------------------------------
|
|
||||||
{output_directory}
|
|
||||||
.ytdl-sub-single_song_test-download-archive.json
|
|
||||||
{output_directory}/YouTube/[2019] YouTube Rewind 2019: For the Record | #YouTubeRewind
|
|
||||||
01 - YouTube Rewind 2019: For the Record | #YouTubeRewind.mp3
|
|
||||||
Embedded Thumbnail, Music Tags:
|
|
||||||
album: YouTube Rewind 2019: For the Record | #YouTubeRewind
|
|
||||||
albumartist: YouTube
|
|
||||||
albumartists: YouTube
|
|
||||||
artist: YouTube
|
|
||||||
artists: YouTube
|
|
||||||
genres: Unset
|
|
||||||
title: YouTube Rewind 2019: For the Record | #YouTubeRewind
|
|
||||||
track: 1
|
|
||||||
tracktotal: 1
|
|
||||||
year: 2019
|
|
||||||
folder.jpg
|
|
||||||
Loading…
Reference in a new issue