diff --git a/docker/Dockerfile b/docker/Dockerfile index 4697a1f3..9e821fef 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -5,16 +5,20 @@ FROM ghcr.io/linuxserver/baseimage-alpine:3.16 COPY root/ / RUN apk update --no-cache && \ + apk upgrade --no-cache && \ apk add --repository=http://dl-3.alpinelinux.org/alpine/edge/main/ \ vim \ g++ \ nano \ make \ python3=~3.10 \ + py3-pip \ py3-setuptools && \ apk add --repository=http://dl-3.alpinelinux.org/alpine/edge/community/ \ - ffmpeg \ - py3-pip && \ + libcrypto1.1 \ + libcrypto3 \ + libsrt \ + ffmpeg && \ mkdir -p /config && \ pip install --no-cache-dir ytdl_sub-*.whl && \ rm ytdl_sub-*.whl && \ diff --git a/src/ytdl_sub/plugins/music_tags.py b/src/ytdl_sub/plugins/music_tags.py index b054c30d..f66b20a6 100644 --- a/src/ytdl_sub/plugins/music_tags.py +++ b/src/ytdl_sub/plugins/music_tags.py @@ -1,5 +1,7 @@ +from collections import defaultdict from typing import Any from typing import Dict +from typing import List import mediafile @@ -7,10 +9,40 @@ from ytdl_sub.entries.entry import Entry from ytdl_sub.plugins.plugin import Plugin from ytdl_sub.plugins.plugin import PluginOptions from ytdl_sub.utils.file_handler import FileMetadata +from ytdl_sub.utils.logger import Logger from ytdl_sub.utils.thumbnail import convert_download_thumbnail -from ytdl_sub.validators.string_formatter_validators import DictFormatterValidator +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 StringFormatterValidator from ytdl_sub.validators.validators import BoolValidator +logger = Logger.get("music_tags") + + +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(PluginOptions): """ @@ -32,7 +64,11 @@ class MusicTagsOptions(PluginOptions): tags: artist: "{artist}" album: "{album}" - genre: "ytdl downloaded music" + genre: "ytdl-sub" + # Supports id3v2.4 multi-tags + albumartists: + - "{artist}" + - "ytdl-sub" # Optional embed_thumbnail: False """ @@ -52,15 +88,16 @@ class MusicTagsOptions(PluginOptions): def __init__(self, name, value): super().__init__(name, value) - self._tags = self._validate_key(key="tags", validator=DictFormatterValidator) + self._tags = self._validate_key(key="tags", validator=MusicTagsValidator) self._embed_thumbnail = self._validate_key_if_present( key="embed_thumbnail", validator=BoolValidator, default=False ).value @property - def tags(self) -> DictFormatterValidator: + def tags(self) -> MusicTagsValidator: """ - Key/values of tag names/tag values. Supports source and override variables. + Key, values of tag names, tag values. Supports source and override variables. + Supports lists which will get written to MP3s as id3v2.4 multi-tags. """ return self._tags @@ -79,32 +116,29 @@ class MusicTagsPlugin(Plugin[MusicTagsOptions]): """ Tags the entry's audio file using values defined in the metadata options """ - supported_fields = list(mediafile.MediaFile.sorted_fields()) - tags_to_write: Dict[str, str] = {} - for tag_name, tag_formatter in self.plugin_options.tags.dict.items(): - if tag_name not in supported_fields: - # TODO: Add support for custom fields - self._logger.warning( - "tag '%s' is not supported for %s files. Supported tags: %s", - tag_name, - entry.ext, - ", ".join(sorted(supported_fields)), - ) - continue - - tag_value = self.overrides.apply_formatter(formatter=tag_formatter, entry=entry) - tags_to_write[tag_name] = tag_value + # 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(): + for tag_formatter in tag_formatters: + tag_value = self.overrides.apply_formatter(formatter=tag_formatter, entry=entry) + tags_to_write[tag_name].append(tag_value) # write the actual tags if its not a dry run if not self.is_dry_run: audio_file = mediafile.MediaFile(entry.get_download_file_path()) for tag_name, tag_value in tags_to_write.items(): - # If the attribute is a List-type, set it as the first element of the list + # If the attribute is a List-type, set it as the list type if isinstance(getattr(audio_file, tag_name), list): - setattr(audio_file, tag_name, [tag_value]) + setattr(audio_file, tag_name, tag_value) # Otherwise, set as single value else: - setattr(audio_file, tag_name, tag_value) + if len(tag_value) > 1: + logger.warning( + "Music tag '%s' does not support lists. " + "Only setting the first element", + tag_name, + ) + setattr(audio_file, tag_name, tag_value[0]) if self.plugin_options.embed_thumbnail: # convert the entry thumbnail so it is embedded as jpg diff --git a/tests/e2e/plugins/test_audio_extract.py b/tests/e2e/plugins/test_audio_extract.py index fef552b5..19ef7ae2 100644 --- a/tests/e2e/plugins/test_audio_extract.py +++ b/tests/e2e/plugins/test_audio_extract.py @@ -11,7 +11,8 @@ def single_song_preset_dict(output_directory): "preset": "song", "download": {"url": "https://www.youtube.com/watch?v=2lAe1cqCOXo"}, "output_options": {"output_directory": output_directory}, - "music_tags": {"embed_thumbnail": True}, + # test multi-tags + "music_tags": {"embed_thumbnail": True, "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_playlist.json b/tests/resources/expected_downloads_summaries/plugins/test_audio_extract_playlist.json index 2676cfc4..2ff866ba 100644 --- a/tests/resources/expected_downloads_summaries/plugins/test_audio_extract_playlist.json +++ b/tests/resources/expected_downloads_summaries/plugins/test_audio_extract_playlist.json @@ -1,5 +1,5 @@ { - "Jesse's Minecraft Server [Trailer - Feb.1].ogg": "e8802a80b3011ba4ebdb2fdd8da4f84d", - "Jesse's Minecraft Server [Trailer - Feb.27].ogg": "e9a8a0f2aa98a37610b9418333026f58", - "Jesse's Minecraft Server [Trailer - Mar.21].ogg": "87fd3e42e238374ae1411479d35d38b7" + "Jesse's Minecraft Server [Trailer - Feb.1].ogg": "e0ab90ea97f26738195111de2fbf0f5f", + "Jesse's Minecraft Server [Trailer - Feb.27].ogg": "814bdd627fbd4b55017341ffdec9da9d", + "Jesse's Minecraft Server [Trailer - Mar.21].ogg": "90303a47a312c27cf8eed56bf09aa526" } \ No newline at end of file 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 9e8f03fa..d0e545a8 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,3 +1,3 @@ { - "YouTube Rewind 2019: For the Record | #YouTubeRewind.mp3": "5df65e724b3c305195473c42bbc53da9" + "YouTube Rewind 2019: For the Record | #YouTubeRewind.mp3": "05bd1d218f5a0c942ddb1448d939a1dc" } \ 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 9f610a07..1cb85a75 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 @@ -9,6 +9,9 @@ Files created: artist: YouTube artists: YouTube genre: Unset + genres: + - multi_tag_1 + - multi_tag_2 title: YouTube Rewind 2019: For the Record | #YouTubeRewind track: 1 year: 2019 \ No newline at end of file