[FEATURE] format plugin to set yt-dlp format easier (#714)
Makes setting yt-dlp's `format` field easier by giving it its own documented plugin.
Old:
```
my_format_preset:
ytdl_options:
format: "best"
```
New:
```
my_format_preset:
format: "best"
```
The old method will still work, so do not worry about updating configs ASAP. However, the option is available to save a few lines 😉
This commit is contained in:
parent
792da1ba86
commit
a48efdc84c
19 changed files with 78 additions and 52 deletions
|
|
@ -183,6 +183,12 @@ file_convert
|
|||
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
format
|
||||
''''''
|
||||
.. autoclass:: ytdl_sub.plugins.format.FormatOptions()
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
match_filters
|
||||
'''''''''''''
|
||||
.. autoclass:: ytdl_sub.plugins.match_filters.MatchFiltersOptions()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
44
src/ytdl_sub/plugins/format.py
Normal file
44
src/ytdl_sub/plugins/format.py
Normal file
|
|
@ -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}
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 🎸",
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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: {}})
|
||||
|
|
|
|||
Loading…
Reference in a new issue