[FEATURE] Add mp3 id3v2.4 multi-tag support (#356)

* [FEATURE] Add mp3 id3v2.4 multi-tag support

* docs

* fix new docker issue

* update fixture
This commit is contained in:
Jesse Bannon 2022-11-24 00:56:23 -08:00 committed by GitHub
parent 5d9fe2eaae
commit ead5620c5c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 72 additions and 30 deletions

View file

@ -5,16 +5,20 @@ FROM ghcr.io/linuxserver/baseimage-alpine:3.16
COPY root/ / COPY root/ /
RUN apk update --no-cache && \ RUN apk update --no-cache && \
apk upgrade --no-cache && \
apk add --repository=http://dl-3.alpinelinux.org/alpine/edge/main/ \ apk add --repository=http://dl-3.alpinelinux.org/alpine/edge/main/ \
vim \ vim \
g++ \ g++ \
nano \ nano \
make \ make \
python3=~3.10 \ python3=~3.10 \
py3-pip \
py3-setuptools && \ py3-setuptools && \
apk add --repository=http://dl-3.alpinelinux.org/alpine/edge/community/ \ apk add --repository=http://dl-3.alpinelinux.org/alpine/edge/community/ \
ffmpeg \ libcrypto1.1 \
py3-pip && \ libcrypto3 \
libsrt \
ffmpeg && \
mkdir -p /config && \ mkdir -p /config && \
pip install --no-cache-dir ytdl_sub-*.whl && \ pip install --no-cache-dir ytdl_sub-*.whl && \
rm ytdl_sub-*.whl && \ rm ytdl_sub-*.whl && \

View file

@ -1,5 +1,7 @@
from collections import defaultdict
from typing import Any from typing import Any
from typing import Dict from typing import Dict
from typing import List
import mediafile 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 Plugin
from ytdl_sub.plugins.plugin import PluginOptions from ytdl_sub.plugins.plugin import PluginOptions
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.thumbnail import convert_download_thumbnail 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 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): class MusicTagsOptions(PluginOptions):
""" """
@ -32,7 +64,11 @@ class MusicTagsOptions(PluginOptions):
tags: tags:
artist: "{artist}" artist: "{artist}"
album: "{album}" album: "{album}"
genre: "ytdl downloaded music" genre: "ytdl-sub"
# Supports id3v2.4 multi-tags
albumartists:
- "{artist}"
- "ytdl-sub"
# Optional # Optional
embed_thumbnail: False embed_thumbnail: False
""" """
@ -52,15 +88,16 @@ class MusicTagsOptions(PluginOptions):
def __init__(self, name, value): def __init__(self, name, value):
super().__init__(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( self._embed_thumbnail = self._validate_key_if_present(
key="embed_thumbnail", validator=BoolValidator, default=False key="embed_thumbnail", validator=BoolValidator, default=False
).value ).value
@property @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 return self._tags
@ -79,32 +116,29 @@ class MusicTagsPlugin(Plugin[MusicTagsOptions]):
""" """
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
""" """
supported_fields = list(mediafile.MediaFile.sorted_fields()) # Resolve the tags into this dict
tags_to_write: Dict[str, str] = {} tags_to_write: Dict[str, List[str]] = defaultdict(list)
for tag_name, tag_formatter in self.plugin_options.tags.dict.items(): for tag_name, tag_formatters in self.plugin_options.tags.as_lists.items():
if tag_name not in supported_fields: for tag_formatter in tag_formatters:
# TODO: Add support for custom fields tag_value = self.overrides.apply_formatter(formatter=tag_formatter, entry=entry)
self._logger.warning( tags_to_write[tag_name].append(tag_value)
"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
# write the actual tags if its not a dry run # write the actual tags if its not a dry run
if not self.is_dry_run: if not self.is_dry_run:
audio_file = mediafile.MediaFile(entry.get_download_file_path()) audio_file = mediafile.MediaFile(entry.get_download_file_path())
for tag_name, tag_value in tags_to_write.items(): 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): 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 # Otherwise, set as single value
else: 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: if self.plugin_options.embed_thumbnail:
# convert the entry thumbnail so it is embedded as jpg # convert the entry thumbnail so it is embedded as jpg

View file

@ -11,7 +11,8 @@ def single_song_preset_dict(output_directory):
"preset": "song", "preset": "song",
"download": {"url": "https://www.youtube.com/watch?v=2lAe1cqCOXo"}, "download": {"url": "https://www.youtube.com/watch?v=2lAe1cqCOXo"},
"output_options": {"output_directory": output_directory}, "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 # download the worst format so it is fast
"ytdl_options": { "ytdl_options": {
"format": "worst[ext=mp4]", "format": "worst[ext=mp4]",

View file

@ -1,5 +1,5 @@
{ {
"Jesse's Minecraft Server [Trailer - Feb.1].ogg": "e8802a80b3011ba4ebdb2fdd8da4f84d", "Jesse's Minecraft Server [Trailer - Feb.1].ogg": "e0ab90ea97f26738195111de2fbf0f5f",
"Jesse's Minecraft Server [Trailer - Feb.27].ogg": "e9a8a0f2aa98a37610b9418333026f58", "Jesse's Minecraft Server [Trailer - Feb.27].ogg": "814bdd627fbd4b55017341ffdec9da9d",
"Jesse's Minecraft Server [Trailer - Mar.21].ogg": "87fd3e42e238374ae1411479d35d38b7" "Jesse's Minecraft Server [Trailer - Mar.21].ogg": "90303a47a312c27cf8eed56bf09aa526"
} }

View file

@ -1,3 +1,3 @@
{ {
"YouTube Rewind 2019 For the Record #YouTubeRewind.mp3": "5df65e724b3c305195473c42bbc53da9" "YouTube Rewind 2019 For the Record #YouTubeRewind.mp3": "05bd1d218f5a0c942ddb1448d939a1dc"
} }

View file

@ -9,6 +9,9 @@ Files created:
artist: YouTube artist: YouTube
artists: YouTube artists: YouTube
genre: Unset genre: Unset
genres:
- multi_tag_1
- multi_tag_2
title: YouTube Rewind 2019: For the Record | #YouTubeRewind title: YouTube Rewind 2019: For the Record | #YouTubeRewind
track: 1 track: 1
year: 2019 year: 2019