[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:
parent
5d9fe2eaae
commit
ead5620c5c
6 changed files with 72 additions and 30 deletions
|
|
@ -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 && \
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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]",
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
{
|
{
|
||||||
"YouTube Rewind 2019: For the Record | #YouTubeRewind.mp3": "5df65e724b3c305195473c42bbc53da9"
|
"YouTube Rewind 2019: For the Record | #YouTubeRewind.mp3": "05bd1d218f5a0c942ddb1448d939a1dc"
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
||||||
Loading…
Reference in a new issue