diff --git a/docs/config.rst b/docs/config.rst index 71805d2d..b95a5957 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -183,6 +183,12 @@ file_convert ------------------------------------------------------------------------------- +format +'''''' +.. autoclass:: ytdl_sub.plugins.format.FormatOptions() + +------------------------------------------------------------------------------- + match_filters ''''''''''''' .. autoclass:: ytdl_sub.plugins.match_filters.MatchFiltersOptions() diff --git a/src/ytdl_sub/config/preset_class_mappings.py b/src/ytdl_sub/config/preset_class_mappings.py index 060d8c89..31a96363 100644 --- a/src/ytdl_sub/config/preset_class_mappings.py +++ b/src/ytdl_sub/config/preset_class_mappings.py @@ -8,6 +8,7 @@ from ytdl_sub.plugins.chapters import ChaptersPlugin from ytdl_sub.plugins.date_range import DateRangePlugin from ytdl_sub.plugins.embed_thumbnail import EmbedThumbnailPlugin from ytdl_sub.plugins.file_convert import FileConvertPlugin +from ytdl_sub.plugins.format import FormatPlugin from ytdl_sub.plugins.internal.view import ViewPlugin from ytdl_sub.plugins.match_filters import MatchFiltersPlugin from ytdl_sub.plugins.music_tags import MusicTagsPlugin @@ -30,6 +31,7 @@ class PluginMapping: "date_range": DateRangePlugin, "embed_thumbnail": EmbedThumbnailPlugin, "file_convert": FileConvertPlugin, + "format": FormatPlugin, "match_filters": MatchFiltersPlugin, "music_tags": MusicTagsPlugin, "video_tags": VideoTagsPlugin, diff --git a/src/ytdl_sub/plugins/format.py b/src/ytdl_sub/plugins/format.py new file mode 100644 index 00000000..7f1ccc06 --- /dev/null +++ b/src/ytdl_sub/plugins/format.py @@ -0,0 +1,44 @@ +from typing import Dict +from typing import Optional + +from ytdl_sub.config.plugin import Plugin +from ytdl_sub.config.preset_options import OptionsValidator +from ytdl_sub.validators.validators import StringValidator + + +class FormatOptions(OptionsValidator): + """ + Set ``--format`` to pass into yt-dlp to download a specific format quality. + Uses the same syntax as yt-dlp. + + Usage: + + .. code-block:: yaml + + presets: + my_example_preset: + format: "(bv*[height<=1080]+bestaudio/best[height<=1080])" + """ + + def __init__(self, name, value): + super().__init__(name, value) + self._format = StringValidator(name=name, value=value).value + + @property + def format(self) -> str: + """ + yt-dlp format, uses same syntax as yt-dlp. + """ + return self._format + + +class FormatPlugin(Plugin[FormatOptions]): + plugin_options_type = FormatOptions + + def ytdl_options(self) -> Optional[Dict]: + """ + Returns + ------- + yt-dlp format + """ + return {"format": self.plugin_options.format} diff --git a/src/ytdl_sub/prebuilt_presets/helpers/common.yaml b/src/ytdl_sub/prebuilt_presets/helpers/common.yaml index fda25402..0e00f73d 100644 --- a/src/ytdl_sub/prebuilt_presets/helpers/common.yaml +++ b/src/ytdl_sub/prebuilt_presets/helpers/common.yaml @@ -3,13 +3,13 @@ presets: best_video_quality: + format: "bestvideo+bestaudio/best" ytdl_options: - format: "bestvideo+bestaudio/best" merge_output_format: "mp4" max_1080p: + format: "(bv*[height<=1080]+bestaudio/best[height<=1080])" ytdl_options: - format: "(bv*[height<=1080]+bestaudio/best[height<=1080])" merge_output_format: "mp4" chunk_initial_download: diff --git a/src/ytdl_sub/prebuilt_presets/tv_show/tv_show.yaml b/src/ytdl_sub/prebuilt_presets/tv_show/tv_show.yaml index 99a074cc..c889a053 100644 --- a/src/ytdl_sub/prebuilt_presets/tv_show/tv_show.yaml +++ b/src/ytdl_sub/prebuilt_presets/tv_show/tv_show.yaml @@ -6,8 +6,7 @@ presets: - "_plex_base" - "_episode_base" - "_episode_video_tags" - ytdl_options: - format: "(bv*[ext=mp4][vcodec~='^((he|a)vc|h26[45])']+ba[ext=m4a]) / (bv[ext=mp4]*+ba[ext=m4a]/b)" + format: "(bv*[ext=mp4][vcodec~='^((he|a)vc|h26[45])']+ba[ext=m4a]) / (bv[ext=mp4]*+ba[ext=m4a]/b)" overrides: tv_show_poster_file_name: "poster.jpg" tv_show_fanart_file_name: "fanart.jpg" diff --git a/src/ytdl_sub/subscriptions/subscription_ytdl_options.py b/src/ytdl_sub/subscriptions/subscription_ytdl_options.py index 4ce74409..ab492a85 100644 --- a/src/ytdl_sub/subscriptions/subscription_ytdl_options.py +++ b/src/ytdl_sub/subscriptions/subscription_ytdl_options.py @@ -12,6 +12,7 @@ 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.format import FormatPlugin from ytdl_sub.plugins.match_filters import MatchFiltersPlugin from ytdl_sub.plugins.subtitles import SubtitlesPlugin from ytdl_sub.utils.ffmpeg import FFMPEG @@ -107,6 +108,7 @@ class SubscriptionYTDLOptions: self._global_options, self._output_options, self._plugin_ytdl_options(DateRangePlugin), + self._plugin_ytdl_options(FormatPlugin), self._user_ytdl_options, # user ytdl options... self._info_json_only_options, # then info_json_only options ) @@ -126,6 +128,7 @@ class SubscriptionYTDLOptions: self._plugin_ytdl_options(SubtitlesPlugin), self._plugin_ytdl_options(ChaptersPlugin), self._plugin_ytdl_options(AudioExtractPlugin), + self._plugin_ytdl_options(FormatPlugin), self._user_ytdl_options, # user ytdl options... self._download_only_options, # then download_only options ) diff --git a/tests/e2e/plugins/test_audio_extract.py b/tests/e2e/plugins/test_audio_extract.py index 4f6c3e56..52277921 100644 --- a/tests/e2e/plugins/test_audio_extract.py +++ b/tests/e2e/plugins/test_audio_extract.py @@ -11,9 +11,8 @@ def single_song_preset_dict_old_format(output_directory): "preset": "single", # test multi-tags "music_tags": {"embed_thumbnail": True, "tags": {"genres": ["multi_tag_1", "multi_tag_2"]}}, - # download the worst format so it is fast + "format": "worst[ext=mp4]", # download the worst format so it is fast "ytdl_options": { - "format": "worst[ext=mp4]", "postprocessor_args": {"ffmpeg": ["-bitexact"]}, # Must add this for reproducibility }, "overrides": { @@ -31,9 +30,8 @@ def single_song_preset_dict(output_directory): "music_tags": {"genres": ["multi_tag_1", "multi_tag_2"]}, # test the new embed_thumbnail plugin "embed_thumbnail": True, - # download the worst format so it is fast + "format": "worst[ext=mp4]", # download the worst format so it is fast "ytdl_options": { - "format": "worst[ext=mp4]", "postprocessor_args": {"ffmpeg": ["-bitexact"]}, # Must add this for reproducibility }, "overrides": { @@ -53,9 +51,8 @@ def multiple_songs_preset_dict(output_directory): return { "preset": "albums_from_playlists", "audio_extract": {"codec": "vorbis", "quality": 140}, - # download the worst format so it is fast + "format": "worst[ext=mp4]", # download the worst format so it is fast "ytdl_options": { - "format": "worst[ext=mp4]", "postprocessor_args": {"ffmpeg": ["-bitexact"]}, # Must add this for reproducibility }, "overrides": { diff --git a/tests/e2e/plugins/test_chapters.py b/tests/e2e/plugins/test_chapters.py index 93d8aeb9..291e2e8f 100644 --- a/tests/e2e/plugins/test_chapters.py +++ b/tests/e2e/plugins/test_chapters.py @@ -35,9 +35,8 @@ def sponsorblock_and_subs_preset_dict(output_directory) -> Dict: "Outro", ], }, - # download the worst format so it is fast + "format": "worst[ext=mp4]", # download the worst format so it is fast "ytdl_options": { - "format": "worst[ext=mp4]", "postprocessor_args": {"ffmpeg": ["-bitexact"]}, # Must add this for reproducibility }, "overrides": {"artist": "JMC"}, diff --git a/tests/e2e/plugins/test_date_range.py b/tests/e2e/plugins/test_date_range.py index 47bf1b73..f1f8bccc 100644 --- a/tests/e2e/plugins/test_date_range.py +++ b/tests/e2e/plugins/test_date_range.py @@ -15,9 +15,8 @@ def recent_preset_dict(output_directory): return { "preset": "tv_show", "date_range": {"after": "20150101"}, - # download the worst format so it is fast + "format": "worst[ext=mp4]", # download the worst format so it is fast "ytdl_options": { - "format": "worst[ext=mp4]", "max_views": 100000, # do not download the popular PJ concert }, "overrides": { diff --git a/tests/e2e/plugins/test_music_tags.py b/tests/e2e/plugins/test_music_tags.py index 586bea3b..82d08956 100644 --- a/tests/e2e/plugins/test_music_tags.py +++ b/tests/e2e/plugins/test_music_tags.py @@ -13,9 +13,8 @@ def single_song_video_dict(output_directory): "output_options": {"output_directory": output_directory, "file_name": "will_error.mp4"}, # test multi-tags "music_tags": {"genres": ["multi_tag_1", "multi_tag_2"]}, - # download the worst format so it is fast + "format": "worst[ext=mp4]", # download the worst format so it is fast "ytdl_options": { - "format": "worst[ext=mp4]", "postprocessor_args": {"ffmpeg": ["-bitexact"]}, # Must add this for reproducibility }, } diff --git a/tests/e2e/plugins/test_nfo_tags.py b/tests/e2e/plugins/test_nfo_tags.py index d226b8ba..8c3addaf 100644 --- a/tests/e2e/plugins/test_nfo_tags.py +++ b/tests/e2e/plugins/test_nfo_tags.py @@ -11,10 +11,7 @@ def subscription_dict(output_directory): "download": "https://www.youtube.com/shorts/ucYmEqmlhFw", # override the output directory with our fixture-generated dir "output_options": {"output_directory": output_directory}, - # download the worst format so it is fast - "ytdl_options": { - "format": "best[height<=480]", - }, + "format": "best[height<=480]", # download the worst format so it is fast "nfo_tags": { "tags": { "kodi_safe_title 🎸": "kodi_safe_value 🎸", diff --git a/tests/e2e/plugins/test_regex.py b/tests/e2e/plugins/test_regex.py index c9c575a4..a081adee 100644 --- a/tests/e2e/plugins/test_regex.py +++ b/tests/e2e/plugins/test_regex.py @@ -20,10 +20,7 @@ def regex_subscription_dict_base(output_directory): "download": "https://youtube.com/playlist?list=PL5BC0FC26BECA5A35", # override the output directory with our fixture-generated dir "output_options": {"output_directory": output_directory}, - # download the worst format so it is fast - "ytdl_options": { - "format": "best[height<=480]", - }, + "format": "best[height<=480]", # download the worst format so it is fast "regex": { # tests that skip_if_match_fails defaults to True "from": { diff --git a/tests/e2e/plugins/test_split_by_chapters.py b/tests/e2e/plugins/test_split_by_chapters.py index d1085e11..a7a39ae9 100644 --- a/tests/e2e/plugins/test_split_by_chapters.py +++ b/tests/e2e/plugins/test_split_by_chapters.py @@ -13,9 +13,8 @@ from ytdl_sub.utils.exceptions import ValidationException def yt_album_as_chapters_preset_dict(output_directory): return { "preset": "albums_from_chapters", - # download the worst format so it is fast + "format": "worst[ext=mp4]", # download the worst format so it is fast "ytdl_options": { - "format": "worst[ext=mp4]", "postprocessor_args": {"ffmpeg": ["-bitexact"]}, # Must add this for reproducibility }, "overrides": { diff --git a/tests/e2e/plugins/test_subtitles.py b/tests/e2e/plugins/test_subtitles.py index 350b45c4..a8d6e840 100644 --- a/tests/e2e/plugins/test_subtitles.py +++ b/tests/e2e/plugins/test_subtitles.py @@ -18,10 +18,7 @@ def single_video_subs_embed_preset_dict(output_directory): "languages": ["en", "de"], "allow_auto_generated_subtitles": True, }, - # download the worst format so it is fast - "ytdl_options": { - "format": "worst[ext=mp4]", - }, + "format": "worst[ext=mp4]", # download the worst format so it is fast "overrides": {"artist": "JMC"}, } diff --git a/tests/e2e/soundcloud/test_soundcloud_discography.py b/tests/e2e/soundcloud/test_soundcloud_discography.py index f435245e..6788118d 100644 --- a/tests/e2e/soundcloud/test_soundcloud_discography.py +++ b/tests/e2e/soundcloud/test_soundcloud_discography.py @@ -9,10 +9,7 @@ from ytdl_sub.subscriptions.subscription import Subscription def subscription_dict(output_directory): return { "preset": "soundcloud_discography", - # download the worst format so it is fast - "ytdl_options": { - "format": "worst[ext=mp3]", - }, + "format": "worst[ext=mp3]", # download the worst format so it is fast "overrides": { "track_artist": "j_b", "sc_artist_url": "https://soundcloud.com/jessebannon", diff --git a/tests/e2e/youtube/test_channel.py b/tests/e2e/youtube/test_channel.py index f686277c..b9481521 100644 --- a/tests/e2e/youtube/test_channel.py +++ b/tests/e2e/youtube/test_channel.py @@ -9,8 +9,8 @@ from ytdl_sub.subscriptions.subscription import Subscription def channel_preset_dict(output_directory): return { "preset": "tv_show", + "format": "worst[ext=mp4]", # download the worst format so it is fast "ytdl_options": { - "format": "worst[ext=mp4]", # download the worst format so it is fast "max_views": 100000, # do not download the popular PJ concert "break_on_reject": False, # do not break from max views }, diff --git a/tests/e2e/youtube/test_playlist.py b/tests/e2e/youtube/test_playlist.py index 8235ce2c..4a186c9e 100644 --- a/tests/e2e/youtube/test_playlist.py +++ b/tests/e2e/youtube/test_playlist.py @@ -16,10 +16,7 @@ def playlist_preset_dict(output_directory): "season_by_collection__episode_by_year_month_day", "collection_season_1", ], - # download the worst format so it is fast - "ytdl_options": { - "format": "worst[ext=mp4]", - }, + "format": "worst[ext=mp4]", # download the worst format so it is fast "output_directory_nfo_tags": { "nfo_name": "tvshow.nfo", "nfo_root": "test", diff --git a/tests/e2e/youtube/test_video.py b/tests/e2e/youtube/test_video.py index 00f05144..f9fd9e33 100644 --- a/tests/e2e/youtube/test_video.py +++ b/tests/e2e/youtube/test_video.py @@ -23,12 +23,8 @@ def single_video_preset_dict_old_video_tags_format(output_directory): "output_directory": output_directory, "maintain_download_archive": False, }, - # embed thumb into the video - "embed_thumbnail": True, - # download the worst format so it is fast - "ytdl_options": { - "format": "worst[ext=mp4]", - }, + "embed_thumbnail": True, # embed thumb into the video + "format": "worst[ext=mp4]", # download the worst format so it is fast # also test video tags "video_tags": { "tags": { @@ -49,12 +45,8 @@ def single_video_preset_dict(output_directory): "output_directory": output_directory, "maintain_download_archive": False, }, - # embed thumb into the video - "embed_thumbnail": True, - # download the worst format so it is fast - "ytdl_options": { - "format": "worst[ext=mp4]", - }, + "embed_thumbnail": True, # embed thumb into the video + "format": "worst[ext=mp4]", # download the worst format so it is fast # also test video tags "video_tags": { "title": "{title}", @@ -76,8 +68,8 @@ def single_tv_show_video_nulled_values_preset_dict(output_directory): "thumbnail_name": "", "info_json_name": "", }, + "format": "worst[ext=mp4]", "ytdl_options": { - "format": "worst[ext=mp4]", "max_downloads": 2, }, # test override variables added by ytdl-sub diff --git a/tests/unit/config/test_config_file.py b/tests/unit/config/test_config_file.py index f2716c7d..6dcb6769 100644 --- a/tests/unit/config/test_config_file.py +++ b/tests/unit/config/test_config_file.py @@ -38,6 +38,7 @@ class TestConfigFilePartiallyValidatesPresets: {"output_options": {"file_name": "test"}}, {"output_options": {"keep_files_after": "today"}}, {"ytdl_options": {"format": "best"}}, + {"format": "best"}, {"overrides": {"a": "b"}}, ], ) @@ -52,6 +53,7 @@ class TestConfigFilePartiallyValidatesPresets: def test_success__empty_plugins(self, plugin: str): excluded_plugins = [ "embed_thumbnail", # value is bool, not dict + "format", # value is string, not dict ] if plugin not in excluded_plugins: self._partial_validate({plugin: {}})