From 9f6b3d464715ce4e55ac2bbd97e009cd878b12ab Mon Sep 17 00:00:00 2001 From: Jesse Bannon Date: Mon, 3 Oct 2022 15:19:24 -0700 Subject: [PATCH] [FEATURE] `file_convert` plugin (#267) --- src/ytdl_sub/config/preset_class_mappings.py | 2 + src/ytdl_sub/plugins/file_convert.py | 84 +++++++++++++++++++ tests/e2e/plugins/test_file_convert.py | 47 +++++++++++ .../plugins/file_convert/output.json | 6 ++ .../plugins/file_convert/output.txt | 14 ++++ 5 files changed, 153 insertions(+) create mode 100644 src/ytdl_sub/plugins/file_convert.py create mode 100644 tests/e2e/plugins/test_file_convert.py create mode 100644 tests/resources/expected_downloads_summaries/plugins/file_convert/output.json create mode 100644 tests/resources/transaction_log_summaries/plugins/file_convert/output.txt diff --git a/src/ytdl_sub/config/preset_class_mappings.py b/src/ytdl_sub/config/preset_class_mappings.py index 52867a1f..3a90ddc6 100644 --- a/src/ytdl_sub/config/preset_class_mappings.py +++ b/src/ytdl_sub/config/preset_class_mappings.py @@ -13,6 +13,7 @@ from ytdl_sub.downloaders.youtube.video import YoutubeVideoDownloader from ytdl_sub.plugins.audio_extract import AudioExtractPlugin from ytdl_sub.plugins.chapters import ChaptersPlugin from ytdl_sub.plugins.date_range import DateRangePlugin +from ytdl_sub.plugins.file_convert import FileConvertPlugin from ytdl_sub.plugins.music_tags import MusicTagsPlugin from ytdl_sub.plugins.nfo_tags import NfoTagsPlugin from ytdl_sub.plugins.output_directory_nfo_tags import OutputDirectoryNfoTagsPlugin @@ -117,6 +118,7 @@ class PluginMapping: _MAPPING: Dict[str, Type[Plugin]] = { "audio_extract": AudioExtractPlugin, "date_range": DateRangePlugin, + "file_convert": FileConvertPlugin, "music_tags": MusicTagsPlugin, "video_tags": VideoTagsPlugin, "nfo_tags": NfoTagsPlugin, diff --git a/src/ytdl_sub/plugins/file_convert.py b/src/ytdl_sub/plugins/file_convert.py new file mode 100644 index 00000000..6a33fd4f --- /dev/null +++ b/src/ytdl_sub/plugins/file_convert.py @@ -0,0 +1,84 @@ +from typing import Dict +from typing import Optional + +from ytdl_sub.entries.entry import Entry +from ytdl_sub.plugins.plugin import Plugin +from ytdl_sub.plugins.plugin import PluginOptions +from ytdl_sub.plugins.plugin import PluginPriority +from ytdl_sub.utils.ffmpeg import FFMPEG +from ytdl_sub.utils.file_handler import FileHandler +from ytdl_sub.utils.file_handler import FileMetadata +from ytdl_sub.validators.validators import LiteralDictValidator + + +class FileConvertOptions(PluginOptions): + """ + Converts media files from one extension to another. + + Usage: + + .. code-block:: yaml + + presets: + my_example_preset: + file_converter: + convert: + webm: "mp4" + """ + + _required_keys = {"convert"} + + def __init__(self, name, value): + super().__init__(name, value) + self._convert = self._validate_key(key="convert", validator=LiteralDictValidator).dict + + @property + def convert(self) -> Dict[str, str]: + """ + Convert from one extension to another + """ + return self._convert + + +class FileConvertPlugin(Plugin[FileConvertOptions]): + plugin_options_type = FileConvertOptions + priority = PluginPriority(modify_entry=0) + + def modify_entry(self, entry: Entry) -> Entry: + """ + If the entry is of the specified file type, convert it + """ + for convert_from, convert_to in self.plugin_options.convert.items(): + # Entry ext does not need conversion + if entry.ext != convert_from: + continue + + # Entry ext needs conversion + input_file_path = entry.get_download_file_path() + output_file_path = input_file_path.removesuffix(entry.ext) + convert_to + + ffmpeg_args = ["-bitexact", "-i", input_file_path, output_file_path] + + if not self.is_dry_run: + FFMPEG.run(ffmpeg_args) + FileHandler.delete(input_file_path) + + entry.add_kwargs( + { + "ext": convert_to, + "__converted_from": convert_from, + } + ) + + return entry + + return entry + + def post_process_entry(self, entry: Entry) -> Optional[FileMetadata]: + """ + Add metadata about conversion if it happened + """ + if converted_from := entry.kwargs_get("__converted_from"): + return FileMetadata(f"Converted from {converted_from}") + + return None diff --git a/tests/e2e/plugins/test_file_convert.py b/tests/e2e/plugins/test_file_convert.py new file mode 100644 index 00000000..0826e4bc --- /dev/null +++ b/tests/e2e/plugins/test_file_convert.py @@ -0,0 +1,47 @@ +import pytest +from expected_download import assert_expected_downloads +from expected_transaction_log import assert_transaction_log_matches + +from ytdl_sub.subscriptions.subscription import Subscription + + +@pytest.fixture +def preset_dict(output_directory): + return { + "preset": "yt_music_video", + "youtube": {"video_url": "https://www.youtube.com/watch?v=2zYF9JLHDmA"}, + "output_options": {"output_directory": output_directory}, + # download the worst format so it is fast + "ytdl_options": { + "postprocessor_args": {"ffmpeg": ["-bitexact"]}, # Must add this for reproducibility + }, + "file_convert": {"convert": {"webm": "mp4"}}, + } + + +class TestFileConvert: + @pytest.mark.parametrize("dry_run", [True, False]) + def test_file_convert( + self, + music_video_config, + preset_dict, + output_directory, + dry_run, + ): + subscription = Subscription.from_dict( + config=music_video_config, + preset_name="file_convert_test", + preset_dict=preset_dict, + ) + + 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/file_convert/output.txt", + ) + assert_expected_downloads( + output_directory=output_directory, + dry_run=dry_run, + expected_download_summary_file_name="plugins/file_convert/output.json", + ) diff --git a/tests/resources/expected_downloads_summaries/plugins/file_convert/output.json b/tests/resources/expected_downloads_summaries/plugins/file_convert/output.json new file mode 100644 index 00000000..7dd0043a --- /dev/null +++ b/tests/resources/expected_downloads_summaries/plugins/file_convert/output.json @@ -0,0 +1,6 @@ +{ + "Beyond The Guitar - When you hear Hugh Jackman is returning as Wolverine in Deadpool 3-thumb.jpg": "662fcaadf6e80d63591bac19a5fdffb0", + "Beyond The Guitar - When you hear Hugh Jackman is returning as Wolverine in Deadpool 3.info.json": "c0cdbc58669859a3f85e1bd4f0d1c424", + "Beyond The Guitar - When you hear Hugh Jackman is returning as Wolverine in Deadpool 3.mp4": "14d83cb2ee79d372b3b862d95c2f7a43", + "Beyond The Guitar - When you hear Hugh Jackman is returning as Wolverine in Deadpool 3.nfo": "cacf09ab38f9b3085da9c5af516cf22a" +} \ No newline at end of file diff --git a/tests/resources/transaction_log_summaries/plugins/file_convert/output.txt b/tests/resources/transaction_log_summaries/plugins/file_convert/output.txt new file mode 100644 index 00000000..3d939260 --- /dev/null +++ b/tests/resources/transaction_log_summaries/plugins/file_convert/output.txt @@ -0,0 +1,14 @@ +Files created: +---------------------------------------- +{output_directory} + Beyond The Guitar - When you hear Hugh Jackman is returning as Wolverine in Deadpool 3-thumb.jpg + Beyond The Guitar - When you hear Hugh Jackman is returning as Wolverine in Deadpool 3.info.json + Beyond The Guitar - When you hear Hugh Jackman is returning as Wolverine in Deadpool 3.mp4 + Converted from webm + Beyond The Guitar - When you hear Hugh Jackman is returning as Wolverine in Deadpool 3.nfo + NFO tags: + musicvideo: + album: Music Videos + artist: Beyond The Guitar + title: When you hear Hugh Jackman is returning as Wolverine in Deadpool 3 + year: 2022 \ No newline at end of file