From ad540f6005efaf80beab1829efe7cbeeb90ab6c8 Mon Sep 17 00:00:00 2001 From: Jesse Bannon Date: Mon, 24 Jul 2023 12:45:25 -0700 Subject: [PATCH] [FEATURE] Simplify ``music_tags`` plugin --- docs/deprecation_notices.rst | 13 +++-- examples/music_audio_config.yaml | 24 +++++---- src/ytdl_sub/plugins/music_tags.py | 51 +++++++++++-------- tests/e2e/plugins/test_audio_extract.py | 48 ++++++++++++++++- tests/e2e/plugins/test_music_tags.py | 2 +- .../plugins/test_audio_extract_single.json | 2 +- .../test_audio_extract_single_old_format.json | 5 ++ .../plugins/test_audio_extract_single.txt | 2 +- .../test_audio_extract_single_old_format.txt | 12 +++++ 9 files changed, 119 insertions(+), 40 deletions(-) create mode 100644 tests/resources/expected_downloads_summaries/plugins/test_audio_extract_single_old_format.json create mode 100644 tests/resources/transaction_log_summaries/plugins/test_audio_extract_single_old_format.txt diff --git a/docs/deprecation_notices.rst b/docs/deprecation_notices.rst index 2a05181c..e9875915 100644 --- a/docs/deprecation_notices.rst +++ b/docs/deprecation_notices.rst @@ -4,17 +4,19 @@ Deprecation Notices July 2023 --------- -embed_thumbnail -^^^^^^^^^^^^^^^ +music_tags +^^^^^^^^^^ -Embedding thumbnails has its own dedicated plugin now, which supports both audio and video files. -It will be removed from ``music_tags`` in October 2023. Convert from: +Music tags is getting simplified. ``tags`` will now reside directly under music_tags, and +``embed_thumbnail`` is getting moved to its own plugin (supports video files as well). Convert from: .. code-block:: yaml my_example_preset: music_tags: embed_thumbnail: True + tags: + artist: "Elvis Presley" To the following: @@ -22,4 +24,7 @@ To the following: my_example_preset: embed_thumbnail: True + music_tags: + artist: "Elvis Presley" +The old format will be removed in October 2023. diff --git a/examples/music_audio_config.yaml b/examples/music_audio_config.yaml index 70bc40fb..b9b30bce 100644 --- a/examples/music_audio_config.yaml +++ b/examples/music_audio_config.yaml @@ -64,17 +64,19 @@ presets: # It is recommended to keep most of this as-is, and use override # variables to set them to be what you want. music_tags: - tags: - artist: "{track_artist}" - artists: "{track_artist}" - albumartist: "{track_artist}" - albumartists: "{track_artist}" - title: "{track_title}" - album: "{track_album}" - track: "{track_number}" - tracktotal: "{track_total}" - year: "{track_year}" - genre: "{track_genre}" + artist: "{track_artist}" + artists: "{track_artist}" + albumartist: "{track_artist}" + albumartists: "{track_artist}" + title: "{track_title}" + album: "{track_album}" + track: "{track_number}" + tracktotal: "{track_total}" + year: "{track_year}" + genre: "{track_genre}" + + # Optionally embed the thumbnail into the track + embed_thumbnail: False # For every configurable field, make it an override variable, # so we can carefully tune different use-cases by only modifying override variables. diff --git a/src/ytdl_sub/plugins/music_tags.py b/src/ytdl_sub/plugins/music_tags.py index 4f671d26..6eae0b1c 100644 --- a/src/ytdl_sub/plugins/music_tags.py +++ b/src/ytdl_sub/plugins/music_tags.py @@ -1,5 +1,5 @@ from collections import defaultdict -from typing import Any +from typing import Any, Optional from typing import Dict from typing import List @@ -72,25 +72,27 @@ class MusicTagsOptions(OptionsDictValidator): - "ytdl-sub" """ - _required_keys = {"tags"} - _optional_keys = {"embed_thumbnail"} - - @classmethod - def partial_validate(cls, name: str, value: Any) -> None: - """ - Partially validate music tags - """ - if isinstance(value, dict): - value["tags"] = value.get("tags", {}) - _ = cls(name, value) + _optional_keys = {"tags", "embed_thumbnail"} + _allow_extra_keys = True def __init__(self, name, value): super().__init__(name, value) - self._tags = self._validate_key(key="tags", validator=MusicTagsValidator) + tags_validator: Optional[MusicTagsValidator] = self._validate_key_if_present( + key="tags", validator=MusicTagsValidator + ) self._embed_thumbnail = self._validate_key_if_present( - key="embed_thumbnail", validator=BoolValidator, default=False - ).value + key="embed_thumbnail", validator=BoolValidator + ) + + # New format where tags are the keys, no "tags" or "embed_thumbnail" present + if tags_validator is None and self._embed_thumbnail is None: + self._tags = MusicTagsValidator(name=name, value=value) + self._is_old_format = False + else: + self._is_old_format = True + self._tags = tags_validator or MusicTagsValidator(name=name, value={}) + @property def tags(self) -> MusicTagsValidator: @@ -105,7 +107,9 @@ class MusicTagsOptions(OptionsDictValidator): """ Optional. Whether to embed the thumbnail into the audio file. """ - return self._embed_thumbnail + if self._embed_thumbnail is None: + return False + return self._embed_thumbnail.value class MusicTagsPlugin(Plugin[MusicTagsOptions]): @@ -122,6 +126,17 @@ class MusicTagsPlugin(Plugin[MusicTagsOptions]): f"to audio using the audio_extract plugin." ) + 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." + ) + 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 tags_to_write: Dict[str, List[str]] = defaultdict(list) for tag_name, tag_formatters in self.plugin_options.tags.as_lists.items(): @@ -147,10 +162,6 @@ class MusicTagsPlugin(Plugin[MusicTagsOptions]): setattr(audio_file, tag_name, tag_value[0]) if self.plugin_options.embed_thumbnail: - logger.warning( - "music_tags.embed_thumbnail is now deprecated. Use the dedicated " - "embed_thumbnail plugin instead. This will be removed in October of 2023." - ) # convert the entry thumbnail so it is embedded as jpg convert_download_thumbnail(entry=entry) diff --git a/tests/e2e/plugins/test_audio_extract.py b/tests/e2e/plugins/test_audio_extract.py index 48aeb9d0..136b0cec 100644 --- a/tests/e2e/plugins/test_audio_extract.py +++ b/tests/e2e/plugins/test_audio_extract.py @@ -6,7 +6,7 @@ from ytdl_sub.subscriptions.subscription import Subscription @pytest.fixture -def single_song_preset_dict(output_directory): +def single_song_preset_dict_old_format(output_directory): return { "preset": "single", # test multi-tags @@ -24,6 +24,24 @@ def single_song_preset_dict(output_directory): }, } +@pytest.fixture +def single_song_preset_dict(output_directory): + return { + "preset": "single", + # test multi-tags + "music_tags": {"genres": ["multi_tag_1", "multi_tag_2"]}, + # test the new embed_thumbnail plugin + "embed_thumbnail": True, + # download the worst format so it is fast + "ytdl_options": { + "format": "worst[ext=mp4]", + "postprocessor_args": {"ffmpeg": ["-bitexact"]}, # Must add this for reproducibility + }, + "overrides": { + "url": "https://www.youtube.com/watch?v=2lAe1cqCOXo", + "music_directory": output_directory, + }, + } @pytest.fixture def multiple_songs_preset_dict(output_directory): @@ -44,7 +62,33 @@ def multiple_songs_preset_dict(output_directory): class TestAudioExtract: @pytest.mark.parametrize("dry_run", [True, False]) - def test_audio_extract_single_song( + def test_audio_extract_single_song_old_format( + self, + music_audio_config, + single_song_preset_dict_old_format, + output_directory, + dry_run, + ): + subscription = Subscription.from_dict( + config=music_audio_config, + preset_name="single_song_test", + preset_dict=single_song_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", [True, False]) + def test_audio_extract_single_song_new_format( self, music_audio_config, single_song_preset_dict, diff --git a/tests/e2e/plugins/test_music_tags.py b/tests/e2e/plugins/test_music_tags.py index 5538357b..702015ca 100644 --- a/tests/e2e/plugins/test_music_tags.py +++ b/tests/e2e/plugins/test_music_tags.py @@ -15,7 +15,7 @@ def single_song_video_dict(output_directory): }, "output_options": {"output_directory": output_directory, "file_name": "will_error.mp4"}, # test multi-tags - "music_tags": {"embed_thumbnail": True, "tags": {"genres": ["multi_tag_1", "multi_tag_2"]}}, + "music_tags": {"genres": ["multi_tag_1", "multi_tag_2"]}, # download the worst format so it is fast "ytdl_options": { "format": "worst[ext=mp4]", diff --git a/tests/resources/expected_downloads_summaries/plugins/test_audio_extract_single.json b/tests/resources/expected_downloads_summaries/plugins/test_audio_extract_single.json index f4aa6b67..2a4f44d8 100644 --- a/tests/resources/expected_downloads_summaries/plugins/test_audio_extract_single.json +++ b/tests/resources/expected_downloads_summaries/plugins/test_audio_extract_single.json @@ -1,5 +1,5 @@ { ".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": "37b38834eda1293ad503e00dcff7c4dc", + "YouTube/[2019] YouTube Rewind 2019: For the Record | #YouTubeRewind/01 - YouTube Rewind 2019: For the Record | #YouTubeRewind.mp3": "33c69b0dce605e9f78fe23246ce442dd", "YouTube/[2019] YouTube Rewind 2019: For the Record | #YouTubeRewind/folder.jpg": "50ee47c80f679029f5d3503bb91b045a" } \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/plugins/test_audio_extract_single_old_format.json b/tests/resources/expected_downloads_summaries/plugins/test_audio_extract_single_old_format.json new file mode 100644 index 00000000..0c67e234 --- /dev/null +++ b/tests/resources/expected_downloads_summaries/plugins/test_audio_extract_single_old_format.json @@ -0,0 +1,5 @@ +{ + ".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": "7dcad720d29f5036eb6f75e533757f80", + "YouTube/[2019] YouTube Rewind 2019: For the Record | #YouTubeRewind/folder.jpg": "50ee47c80f679029f5d3503bb91b045a" +} \ No newline at end of file diff --git a/tests/resources/transaction_log_summaries/plugins/test_audio_extract_single.txt b/tests/resources/transaction_log_summaries/plugins/test_audio_extract_single.txt index 5c9b995a..3a12643e 100644 --- a/tests/resources/transaction_log_summaries/plugins/test_audio_extract_single.txt +++ b/tests/resources/transaction_log_summaries/plugins/test_audio_extract_single.txt @@ -4,7 +4,7 @@ Files created: .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: + Music Tags: album: YouTube Rewind 2019: For the Record | #YouTubeRewind albumartist: YouTube albumartists: YouTube diff --git a/tests/resources/transaction_log_summaries/plugins/test_audio_extract_single_old_format.txt b/tests/resources/transaction_log_summaries/plugins/test_audio_extract_single_old_format.txt new file mode 100644 index 00000000..d64f76a1 --- /dev/null +++ b/tests/resources/transaction_log_summaries/plugins/test_audio_extract_single_old_format.txt @@ -0,0 +1,12 @@ +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: + genres: + - multi_tag_1 + - multi_tag_2 + Embedded thumbnail + folder.jpg \ No newline at end of file