[FEATURE] Add video_tags plugin. WIP

This commit is contained in:
jbannon 2022-08-13 06:57:16 +00:00
parent fe2f7186c5
commit 94fc4b59e5
9 changed files with 110 additions and 8 deletions

View file

@ -15,6 +15,7 @@ from ytdl_sub.plugins.output_directory_nfo_tags import OutputDirectoryNfoTagsPlu
from ytdl_sub.plugins.plugin import Plugin from ytdl_sub.plugins.plugin import Plugin
from ytdl_sub.plugins.regex import RegexPlugin from ytdl_sub.plugins.regex import RegexPlugin
from ytdl_sub.plugins.subtitles import SubtitlesPlugin from ytdl_sub.plugins.subtitles import SubtitlesPlugin
from ytdl_sub.plugins.video_tags import VideoTagsPlugin
class DownloadStrategyMapping: class DownloadStrategyMapping:
@ -107,6 +108,7 @@ class PluginMapping:
_MAPPING: Dict[str, Type[Plugin]] = { _MAPPING: Dict[str, Type[Plugin]] = {
"music_tags": MusicTagsPlugin, "music_tags": MusicTagsPlugin,
"video_tags": VideoTagsPlugin,
"nfo_tags": NfoTagsPlugin, "nfo_tags": NfoTagsPlugin,
"output_directory_nfo_tags": OutputDirectoryNfoTagsPlugin, "output_directory_nfo_tags": OutputDirectoryNfoTagsPlugin,
"regex": RegexPlugin, "regex": RegexPlugin,

View file

@ -8,7 +8,7 @@ from ytdl_sub.downloaders.youtube.playlist import YoutubePlaylistDownloaderOptio
from ytdl_sub.entries.youtube import YoutubeVideo from ytdl_sub.entries.youtube import YoutubeVideo
from ytdl_sub.utils.chapters import Chapters from ytdl_sub.utils.chapters import Chapters
from ytdl_sub.utils.chapters import Timestamp from ytdl_sub.utils.chapters import Timestamp
from ytdl_sub.utils.ffmpeg import add_ffmpeg_metadata from ytdl_sub.utils.ffmpeg import set_ffmpeg_metadata_chapters
from ytdl_sub.utils.file_handler import FileMetadata from ytdl_sub.utils.file_handler import FileMetadata
from ytdl_sub.validators.validators import BoolValidator from ytdl_sub.validators.validators import BoolValidator
@ -115,7 +115,7 @@ class YoutubeMergePlaylistDownloader(
chapters = Chapters(timestamps=timestamps, titles=titles) chapters = Chapters(timestamps=timestamps, titles=titles)
if not self.is_dry_run and add_chapters: if not self.is_dry_run and add_chapters:
add_ffmpeg_metadata( set_ffmpeg_metadata_chapters(
file_path=merged_video.get_download_file_path(), file_path=merged_video.get_download_file_path(),
chapters=chapters, chapters=chapters,
file_duration_sec=merged_video.kwargs("duration"), file_duration_sec=merged_video.kwargs("duration"),

View file

@ -7,7 +7,7 @@ from ytdl_sub.downloaders.youtube.abc import YoutubeDownloader
from ytdl_sub.downloaders.youtube.abc import YoutubeDownloaderOptions from ytdl_sub.downloaders.youtube.abc import YoutubeDownloaderOptions
from ytdl_sub.entries.youtube import YoutubeVideo from ytdl_sub.entries.youtube import YoutubeVideo
from ytdl_sub.utils.chapters import Chapters from ytdl_sub.utils.chapters import Chapters
from ytdl_sub.utils.ffmpeg import add_ffmpeg_metadata from ytdl_sub.utils.ffmpeg import set_ffmpeg_metadata_chapters
from ytdl_sub.utils.file_handler import FileMetadata from ytdl_sub.utils.file_handler import FileMetadata
from ytdl_sub.validators.url_validator import YoutubeVideoUrlValidator from ytdl_sub.validators.url_validator import YoutubeVideoUrlValidator
from ytdl_sub.validators.validators import StringValidator from ytdl_sub.validators.validators import StringValidator
@ -105,7 +105,7 @@ class YoutubeVideoDownloader(YoutubeDownloader[YoutubeVideoDownloaderOptions, Yo
# Otherwise, add the chapters and return the video + chapter metadata # Otherwise, add the chapters and return the video + chapter metadata
chapters = Chapters.from_file(chapters_file_path=self.download_options.chapter_timestamps) chapters = Chapters.from_file(chapters_file_path=self.download_options.chapter_timestamps)
if not self.is_dry_run: if not self.is_dry_run:
add_ffmpeg_metadata( set_ffmpeg_metadata_chapters(
file_path=video.get_download_file_path(), file_path=video.get_download_file_path(),
chapters=chapters, chapters=chapters,
file_duration_sec=video.kwargs("duration"), file_duration_sec=video.kwargs("duration"),

View file

@ -0,0 +1,62 @@
from typing import Dict
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.ffmpeg import add_ffmpeg_metadata_key_values
from ytdl_sub.utils.file_handler import FileMetadata
from ytdl_sub.validators.string_formatter_validators import DictFormatterValidator
class VideoTagsOptions(PluginOptions):
"""
Adds tags to every downloaded video file using ffmpeg ``-metadata key=value`` args.
Usage:
.. code-block:: yaml
presets:
my_example_preset:
video_tags:
tags:
title: "{title}"
date: "{upload_date}"
description: "{description}"
"""
_required_keys = {"tags"}
def __init__(self, name, value):
super().__init__(name, value)
self._tags = self._validate_key(key="tags", validator=DictFormatterValidator)
@property
def tags(self) -> DictFormatterValidator:
"""
Key/values of tag names/values. Supports source and override variables.
"""
return self._tags
class VideoTagsPlugin(Plugin[VideoTagsOptions]):
plugin_options_type = VideoTagsOptions
def post_process_entry(self, entry: Entry) -> FileMetadata:
"""
Tags the entry's audio file using values defined in the metadata options
"""
tags_to_write: Dict[str, str] = {}
for tag_name, tag_formatter in self.plugin_options.tags.dict.items():
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
if not self.is_dry_run:
add_ffmpeg_metadata_key_values(
file_path=entry.get_download_file_path(),
key_values=tags_to_write,
)
# report the tags written
return FileMetadata.from_dict(value_dict=tags_to_write, title="Video Tags:")

View file

@ -1,6 +1,7 @@
import shutil import shutil
import subprocess import subprocess
import tempfile import tempfile
from typing import Dict
from typing import List from typing import List
from typing import Optional from typing import Optional
@ -88,11 +89,12 @@ def _create_metadata_chapters(chapters: Chapters, file_duration_sec: int) -> Lis
return lines return lines
def add_ffmpeg_metadata( def set_ffmpeg_metadata_chapters(
file_path: str, chapters: Optional[Chapters], file_duration_sec: int file_path: str, chapters: Optional[Chapters], file_duration_sec: int
) -> None: ) -> None:
""" """
Adds ffmetadata to a file. TODO: support more than just chapters Sets ffmetadata chapters to a file. Note that this will (I think) wipe all prior
metadata.
Parameters Parameters
---------- ----------
@ -130,3 +132,24 @@ def add_ffmpeg_metadata(
) )
shutil.move(src=output_file_path, dst=file_path) shutil.move(src=output_file_path, dst=file_path)
def add_ffmpeg_metadata_key_values(file_path: str, key_values: Dict[str, str]) -> None:
"""
Parameters
----------
file_path
File to add metadata key/values to
key_values
The key/values to add
"""
file_path_ext = file_path.split(".")[-1]
output_file_path = f"{file_path}.out.{file_path_ext}"
ffmpeg_args = ["-i", file_path]
for key, value in key_values.items():
ffmpeg_args.extend(["-metadata", f"{key}={value}"])
ffmpeg_args.extend(["-codec", "copy", output_file_path])
FFMPEG.run(ffmpeg_args)
shutil.move(src=output_file_path, dst=file_path)

View file

@ -1,5 +1,5 @@
{ {
"JMC - Oblivion Mod Falcor p.1-thumb.jpg": "fb95b510681676e81c321171fc23143e", "JMC - Oblivion Mod Falcor p.1-thumb.jpg": "fb95b510681676e81c321171fc23143e",
"JMC - Oblivion Mod Falcor p.1.mp4": "931a705864c57d21d6fedebed4af6bbc", "JMC - Oblivion Mod Falcor p.1.mp4": "170bec01308f639da7459c51ec4a1d7e",
"JMC - Oblivion Mod Falcor p.1.nfo": "89f509a8a3d9003e22a9091abeeae5dc" "JMC - Oblivion Mod Falcor p.1.nfo": "89f509a8a3d9003e22a9091abeeae5dc"
} }

View file

@ -1,5 +1,5 @@
{ {
"JMC - Oblivion Mod Falcor p.1-thumb.jpg": "fb95b510681676e81c321171fc23143e", "JMC - Oblivion Mod Falcor p.1-thumb.jpg": "fb95b510681676e81c321171fc23143e",
"JMC - Oblivion Mod Falcor p.1.mp4": "76b8a7dd428e67e5072d003983bb7e33", "JMC - Oblivion Mod Falcor p.1.mp4": "2a9650e2223cc2cf32032beca44a85d4",
"JMC - Oblivion Mod Falcor p.1.nfo": "89f509a8a3d9003e22a9091abeeae5dc" "JMC - Oblivion Mod Falcor p.1.nfo": "89f509a8a3d9003e22a9091abeeae5dc"
} }

View file

@ -2,6 +2,8 @@ Files created in '{output_directory}'
---------------------------------------- ----------------------------------------
JMC - Oblivion Mod Falcor p.1-thumb.jpg JMC - Oblivion Mod Falcor p.1-thumb.jpg
JMC - Oblivion Mod Falcor p.1.mp4 JMC - Oblivion Mod Falcor p.1.mp4
Video Tags:
title: Oblivion Mod "Falcor" p.1
JMC - Oblivion Mod Falcor p.1.nfo JMC - Oblivion Mod Falcor p.1.nfo
NFO tags: NFO tags:
musicvideo: musicvideo:

View file

@ -18,6 +18,12 @@ def single_video_preset_dict(output_directory):
"ytdl_options": { "ytdl_options": {
"format": "worst[ext=mp4]", "format": "worst[ext=mp4]",
}, },
# also test video tags
"video_tags": {
"tags": {
"title": "{title}",
}
},
"overrides": {"artist": "JMC"}, "overrides": {"artist": "JMC"},
} }
@ -47,11 +53,13 @@ class TestYoutubeVideo:
output_directory=output_directory, output_directory=output_directory,
transaction_log=transaction_log, transaction_log=transaction_log,
transaction_log_summary_file_name="youtube/test_video.txt", transaction_log_summary_file_name="youtube/test_video.txt",
regenerate_transaction_log=True,
) )
assert_expected_downloads( assert_expected_downloads(
output_directory=output_directory, output_directory=output_directory,
dry_run=dry_run, dry_run=dry_run,
expected_download_summary_file_name="youtube/test_video.json", expected_download_summary_file_name="youtube/test_video.json",
regenerate_expected_download_summary=True,
) )
@pytest.mark.parametrize("dry_run", [True, False]) @pytest.mark.parametrize("dry_run", [True, False])
@ -90,7 +98,10 @@ class TestYoutubeVideo:
output_directory, output_directory,
dry_run, dry_run,
): ):
# Test chapters and video tags, throw in a video tag with special chars while we are at it
single_video_preset_dict["youtube"]["chapter_timestamps"] = timestamps_file_path single_video_preset_dict["youtube"]["chapter_timestamps"] = timestamps_file_path
single_video_preset_dict["video_tags"]["tags"]["description"] = "🎸 / ' \" \n newline?"
single_video_subscription = Subscription.from_dict( single_video_subscription = Subscription.from_dict(
config=music_video_config, config=music_video_config,
preset_name="music_video_single_video_test", preset_name="music_video_single_video_test",
@ -102,9 +113,11 @@ class TestYoutubeVideo:
output_directory=output_directory, output_directory=output_directory,
transaction_log=transaction_log, transaction_log=transaction_log,
transaction_log_summary_file_name="youtube/test_video_with_chapter_timestamps.txt", transaction_log_summary_file_name="youtube/test_video_with_chapter_timestamps.txt",
regenerate_transaction_log=True,
) )
assert_expected_downloads( assert_expected_downloads(
output_directory=output_directory, output_directory=output_directory,
dry_run=dry_run, dry_run=dry_run,
expected_download_summary_file_name="youtube/test_video_with_chapter_timestamps.json", expected_download_summary_file_name="youtube/test_video_with_chapter_timestamps.json",
regenerate_expected_download_summary=True,
) )