ytdl-sub 2025.8.28__py3-none-any.whl → 2025.11.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ytdl_sub/__init__.py +1 -1
- ytdl_sub/cli/entrypoint.py +6 -1
- ytdl_sub/cli/parsers/cli_to_sub.py +64 -0
- ytdl_sub/cli/parsers/main.py +4 -0
- ytdl_sub/config/defaults.py +3 -1
- ytdl_sub/config/plugin/plugin.py +11 -0
- ytdl_sub/config/plugin/plugin_mapping.py +8 -0
- ytdl_sub/config/plugin/plugin_operation.py +1 -0
- ytdl_sub/downloaders/url/downloader.py +22 -21
- ytdl_sub/downloaders/url/validators.py +17 -0
- ytdl_sub/entries/script/variable_definitions.py +1 -0
- ytdl_sub/plugins/subtitles.py +32 -3
- ytdl_sub/plugins/throttle_protection.py +124 -30
- ytdl_sub/prebuilt_presets/helpers/filter_duration.yaml +2 -3
- ytdl_sub/prebuilt_presets/helpers/throttle_protection.yaml +14 -2
- ytdl_sub/prebuilt_presets/helpers/url.yaml +201 -0
- ytdl_sub/script/utils/type_checking.py +2 -1
- ytdl_sub/subscriptions/subscription_download.py +3 -0
- ytdl_sub/utils/ffmpeg.py +5 -1
- ytdl_sub/utils/file_lock.py +1 -0
- ytdl_sub/validators/string_formatter_validators.py +20 -0
- {ytdl_sub-2025.8.28.dist-info → ytdl_sub-2025.11.8.dist-info}/METADATA +5 -5
- {ytdl_sub-2025.8.28.dist-info → ytdl_sub-2025.11.8.dist-info}/RECORD +27 -26
- {ytdl_sub-2025.8.28.dist-info → ytdl_sub-2025.11.8.dist-info}/WHEEL +0 -0
- {ytdl_sub-2025.8.28.dist-info → ytdl_sub-2025.11.8.dist-info}/entry_points.txt +0 -0
- {ytdl_sub-2025.8.28.dist-info → ytdl_sub-2025.11.8.dist-info}/licenses/LICENSE +0 -0
- {ytdl_sub-2025.8.28.dist-info → ytdl_sub-2025.11.8.dist-info}/top_level.txt +0 -0
ytdl_sub/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__pypi_version__ = "2025.08
|
|
1
|
+
__pypi_version__ = "2025.11.08";__local_version__ = "2025.11.08+2c33c3b"
|
ytdl_sub/cli/entrypoint.py
CHANGED
|
@@ -12,6 +12,7 @@ from yt_dlp.utils import sanitize_filename
|
|
|
12
12
|
from ytdl_sub.cli.output_summary import output_summary
|
|
13
13
|
from ytdl_sub.cli.output_transaction_log import _maybe_validate_transaction_log_file
|
|
14
14
|
from ytdl_sub.cli.output_transaction_log import output_transaction_log
|
|
15
|
+
from ytdl_sub.cli.parsers.cli_to_sub import print_cli_to_sub
|
|
15
16
|
from ytdl_sub.cli.parsers.dl import DownloadArgsParser
|
|
16
17
|
from ytdl_sub.cli.parsers.main import DEFAULT_CONFIG_FILE_NAME
|
|
17
18
|
from ytdl_sub.cli.parsers.main import parser
|
|
@@ -206,6 +207,10 @@ def main() -> List[Subscription]:
|
|
|
206
207
|
|
|
207
208
|
args, extra_args = parser.parse_known_args()
|
|
208
209
|
|
|
210
|
+
if args.subparser == "cli-to-sub":
|
|
211
|
+
print_cli_to_sub(args=extra_args)
|
|
212
|
+
return []
|
|
213
|
+
|
|
209
214
|
# Load the config
|
|
210
215
|
if args.config:
|
|
211
216
|
config = ConfigFile.from_file_path(args.config)
|
|
@@ -263,7 +268,7 @@ def main() -> List[Subscription]:
|
|
|
263
268
|
_view_url_from_cli(config=config, url=args.url, split_chapters=args.split_chapters)
|
|
264
269
|
)
|
|
265
270
|
else:
|
|
266
|
-
raise ValidationException("Must provide one of the commands: sub, dl, view")
|
|
271
|
+
raise ValidationException("Must provide one of the commands: sub, dl, view, cli-to-sub")
|
|
267
272
|
|
|
268
273
|
if not args.suppress_transaction_log:
|
|
269
274
|
output_transaction_log(
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
import yt_dlp
|
|
4
|
+
import yt_dlp.options
|
|
5
|
+
|
|
6
|
+
from ytdl_sub.utils.logger import Logger
|
|
7
|
+
from ytdl_sub.utils.yaml import dump_yaml
|
|
8
|
+
|
|
9
|
+
logger = Logger.get()
|
|
10
|
+
|
|
11
|
+
# pylint: disable=missing-function-docstring
|
|
12
|
+
|
|
13
|
+
##############################################################
|
|
14
|
+
# --- BEGIN ----
|
|
15
|
+
# Copy of https://github.com/yt-dlp/yt-dlp/blob/master/devscripts/cli_to_api.py
|
|
16
|
+
|
|
17
|
+
create_parser = yt_dlp.options.create_parser
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def parse_patched_options(opts):
|
|
21
|
+
|
|
22
|
+
patched_parser = create_parser()
|
|
23
|
+
patched_parser.defaults.update(
|
|
24
|
+
{
|
|
25
|
+
"ignoreerrors": False,
|
|
26
|
+
"retries": 0,
|
|
27
|
+
"fragment_retries": 0,
|
|
28
|
+
"extract_flat": False,
|
|
29
|
+
"concat_playlist": "never",
|
|
30
|
+
"update_self": False,
|
|
31
|
+
}
|
|
32
|
+
)
|
|
33
|
+
yt_dlp.options.create_parser = lambda: patched_parser
|
|
34
|
+
try:
|
|
35
|
+
return yt_dlp.parse_options(opts)
|
|
36
|
+
finally:
|
|
37
|
+
yt_dlp.options.create_parser = create_parser
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
default_opts = parse_patched_options([]).ydl_opts
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def cli_to_api(opts, cli_defaults=False):
|
|
44
|
+
opts = (yt_dlp.parse_options if cli_defaults else parse_patched_options)(opts).ydl_opts
|
|
45
|
+
|
|
46
|
+
diff = {k: v for k, v in opts.items() if default_opts[k] != v}
|
|
47
|
+
if "postprocessors" in diff:
|
|
48
|
+
diff["postprocessors"] = [
|
|
49
|
+
pp for pp in diff["postprocessors"] if pp not in default_opts["postprocessors"]
|
|
50
|
+
]
|
|
51
|
+
return diff
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# --- END ----
|
|
55
|
+
##############################################################
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def print_cli_to_sub(args: List[str]) -> None:
|
|
59
|
+
api_args = cli_to_api(args)
|
|
60
|
+
if not api_args:
|
|
61
|
+
logger.info("Does not resolve to any yt-dlp args")
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
print(dump_yaml({"ytdl_options": api_args}))
|
ytdl_sub/cli/parsers/main.py
CHANGED
|
@@ -221,3 +221,7 @@ view_parser.add_argument(
|
|
|
221
221
|
help="View source variables after splitting by chapters",
|
|
222
222
|
)
|
|
223
223
|
view_parser.add_argument("url", help="URL to view source variables for")
|
|
224
|
+
|
|
225
|
+
###################################################################################################
|
|
226
|
+
# CLI-TO-SUB PARSER
|
|
227
|
+
cli_to_sub_parser = subparsers.add_parser("cli-to-sub")
|
ytdl_sub/config/defaults.py
CHANGED
|
@@ -2,6 +2,8 @@ import os
|
|
|
2
2
|
|
|
3
3
|
from ytdl_sub.utils.system import IS_WINDOWS
|
|
4
4
|
|
|
5
|
+
# pylint: disable=invalid-name
|
|
6
|
+
|
|
5
7
|
|
|
6
8
|
def _existing_path(*paths: str) -> str:
|
|
7
9
|
"""
|
|
@@ -22,7 +24,7 @@ if IS_WINDOWS:
|
|
|
22
24
|
|
|
23
25
|
MAX_FILE_NAME_BYTES = 255
|
|
24
26
|
else:
|
|
25
|
-
DEFAULT_LOCK_DIRECTORY = "
|
|
27
|
+
DEFAULT_LOCK_DIRECTORY = ".ytdl-sub-lock"
|
|
26
28
|
DEFAULT_FFMPEG_PATH = os.getenv(
|
|
27
29
|
"YTDL_SUB_FFMPEG_PATH", _existing_path("/usr/bin/ffmpeg", "/usr/local/bin/ffmpeg")
|
|
28
30
|
)
|
ytdl_sub/config/plugin/plugin.py
CHANGED
|
@@ -119,6 +119,17 @@ class Plugin(BasePlugin[OptionsValidatorT], Generic[OptionsValidatorT], ABC):
|
|
|
119
119
|
"""
|
|
120
120
|
return None
|
|
121
121
|
|
|
122
|
+
def post_completion_entry(self, file_metadata: FileMetadata) -> None:
|
|
123
|
+
"""
|
|
124
|
+
After the entry file is moved to its final location, run this hook.
|
|
125
|
+
|
|
126
|
+
Parameters
|
|
127
|
+
----------
|
|
128
|
+
file_metadata
|
|
129
|
+
Metadata about the completed entry's file download
|
|
130
|
+
"""
|
|
131
|
+
return None
|
|
132
|
+
|
|
122
133
|
def post_process_subscription(self):
|
|
123
134
|
"""
|
|
124
135
|
After all downloaded files have been post-processed, apply a subscription-wide post process
|
|
@@ -92,6 +92,12 @@ class PluginMapping:
|
|
|
92
92
|
EmbedThumbnailPlugin,
|
|
93
93
|
]
|
|
94
94
|
|
|
95
|
+
_ORDER_POST_COMPLETION: List[Type[Plugin]] = [
|
|
96
|
+
# Throttle protection should always be last
|
|
97
|
+
# to not sleep over other logic
|
|
98
|
+
ThrottleProtectionPlugin
|
|
99
|
+
]
|
|
100
|
+
|
|
95
101
|
@classmethod
|
|
96
102
|
def _order_by(
|
|
97
103
|
cls, plugin_types: List[Type[Plugin]], operation: PluginOperation
|
|
@@ -102,6 +108,8 @@ class PluginMapping:
|
|
|
102
108
|
ordering = cls._ORDER_MODIFY_ENTRY
|
|
103
109
|
elif operation == PluginOperation.POST_PROCESS:
|
|
104
110
|
ordering = cls._ORDER_POST_PROCESS
|
|
111
|
+
elif operation == PluginOperation.POST_COMPLETION:
|
|
112
|
+
ordering = cls._ORDER_POST_COMPLETION
|
|
105
113
|
else:
|
|
106
114
|
raise ValueError("PluginOperation does not support ordering")
|
|
107
115
|
|
|
@@ -257,6 +257,16 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
|
|
|
257
257
|
.to_dict()
|
|
258
258
|
)
|
|
259
259
|
|
|
260
|
+
def webpage_url(self, entry: Entry) -> str:
|
|
261
|
+
"""
|
|
262
|
+
Returns
|
|
263
|
+
-------
|
|
264
|
+
The webpage_url to use for the actual download
|
|
265
|
+
"""
|
|
266
|
+
url_idx = entry.get(v.ytdl_sub_input_url_index, int)
|
|
267
|
+
webpage_url_formatter = self.plugin_options.urls.list[url_idx].webpage_url
|
|
268
|
+
return self.overrides.apply_formatter(webpage_url_formatter, entry=entry)
|
|
269
|
+
|
|
260
270
|
def metadata_ytdl_options(self, ytdl_option_overrides: Dict) -> Dict:
|
|
261
271
|
"""
|
|
262
272
|
Returns
|
|
@@ -358,7 +368,7 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
|
|
|
358
368
|
if (self.is_dry_run or not self.is_entry_thumbnails_enabled)
|
|
359
369
|
else entry.is_thumbnail_downloaded_via_ytdlp
|
|
360
370
|
),
|
|
361
|
-
url=
|
|
371
|
+
url=self.webpage_url(entry=entry),
|
|
362
372
|
)
|
|
363
373
|
return Entry(
|
|
364
374
|
download_entry_dict,
|
|
@@ -366,13 +376,13 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
|
|
|
366
376
|
)
|
|
367
377
|
|
|
368
378
|
def _iterate_child_entries(
|
|
369
|
-
self, entries: List[Entry],
|
|
379
|
+
self, entries: List[Entry], validator: UrlValidator
|
|
370
380
|
) -> Iterator[Entry]:
|
|
371
381
|
# Iterate a list of entries, and delete the entries after yielding
|
|
372
382
|
entries_to_iter: List[Optional[Entry]] = entries
|
|
373
383
|
|
|
374
384
|
indices = list(range(len(entries_to_iter)))
|
|
375
|
-
if
|
|
385
|
+
if self.overrides.evaluate_boolean(validator.download_reverse):
|
|
376
386
|
indices = reversed(indices)
|
|
377
387
|
|
|
378
388
|
for idx in indices:
|
|
@@ -394,17 +404,13 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
|
|
|
394
404
|
entries_to_iter[idx] = None
|
|
395
405
|
|
|
396
406
|
def _iterate_parent_entry(
|
|
397
|
-
self, parent: EntryParent,
|
|
407
|
+
self, parent: EntryParent, validator: UrlValidator
|
|
398
408
|
) -> Iterator[Entry]:
|
|
399
|
-
yield from self._iterate_child_entries(
|
|
400
|
-
entries=parent.entry_children(), download_reversed=download_reversed
|
|
401
|
-
)
|
|
409
|
+
yield from self._iterate_child_entries(entries=parent.entry_children(), validator=validator)
|
|
402
410
|
|
|
403
411
|
# Recursion the parent's parent entries
|
|
404
412
|
for parent_child in reversed(parent.parent_children()):
|
|
405
|
-
yield from self._iterate_parent_entry(
|
|
406
|
-
parent=parent_child, download_reversed=download_reversed
|
|
407
|
-
)
|
|
413
|
+
yield from self._iterate_parent_entry(parent=parent_child, validator=validator)
|
|
408
414
|
|
|
409
415
|
def _download_url_metadata(
|
|
410
416
|
self, url: str, include_sibling_metadata: bool, ytdl_options_overrides: Dict
|
|
@@ -438,7 +444,7 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
|
|
|
438
444
|
self,
|
|
439
445
|
parents: List[EntryParent],
|
|
440
446
|
orphans: List[Entry],
|
|
441
|
-
|
|
447
|
+
validator: UrlValidator,
|
|
442
448
|
) -> Iterator[Entry]:
|
|
443
449
|
"""
|
|
444
450
|
Downloads the leaf entries from EntryParent trees
|
|
@@ -446,19 +452,15 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
|
|
|
446
452
|
# Delete info json files afterwards so other collection URLs do not use them
|
|
447
453
|
with self._separate_download_archives(clear_info_json_files=True):
|
|
448
454
|
for parent in parents:
|
|
449
|
-
yield from self._iterate_parent_entry(
|
|
450
|
-
parent=parent, download_reversed=download_reversed
|
|
451
|
-
)
|
|
455
|
+
yield from self._iterate_parent_entry(parent=parent, validator=validator)
|
|
452
456
|
|
|
453
|
-
yield from self._iterate_child_entries(
|
|
454
|
-
entries=orphans, download_reversed=download_reversed
|
|
455
|
-
)
|
|
457
|
+
yield from self._iterate_child_entries(entries=orphans, validator=validator)
|
|
456
458
|
|
|
457
459
|
def _download_metadata(self, url: str, validator: UrlValidator) -> Iterable[Entry]:
|
|
458
460
|
metadata_ytdl_options = self.metadata_ytdl_options(
|
|
459
461
|
ytdl_option_overrides=validator.ytdl_options.to_native_dict(self.overrides)
|
|
460
462
|
)
|
|
461
|
-
|
|
463
|
+
|
|
462
464
|
include_sibling_metadata = self.overrides.evaluate_boolean(
|
|
463
465
|
validator.include_sibling_metadata
|
|
464
466
|
)
|
|
@@ -469,16 +471,15 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
|
|
|
469
471
|
ytdl_options_overrides=metadata_ytdl_options,
|
|
470
472
|
)
|
|
471
473
|
|
|
472
|
-
# TODO: Encapsulate this logic into its own class
|
|
473
474
|
self._url_state = URLDownloadState(
|
|
474
|
-
entries_total=sum(parent.num_children() for parent in parents) + len(orphan_entries)
|
|
475
|
+
entries_total=sum(parent.num_children() for parent in parents) + len(orphan_entries),
|
|
475
476
|
)
|
|
476
477
|
|
|
477
478
|
download_logger.info("Beginning downloads for %s", url)
|
|
478
479
|
yield from self._iterate_entries(
|
|
479
480
|
parents=parents,
|
|
480
481
|
orphans=orphan_entries,
|
|
481
|
-
|
|
482
|
+
validator=validator,
|
|
482
483
|
)
|
|
483
484
|
|
|
484
485
|
def download_metadata(self) -> Iterable[Entry]:
|
|
@@ -52,6 +52,7 @@ class UrlValidator(StrictDictValidator):
|
|
|
52
52
|
"download_reverse",
|
|
53
53
|
"ytdl_options",
|
|
54
54
|
"include_sibling_metadata",
|
|
55
|
+
"webpage_url",
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
@classmethod
|
|
@@ -89,6 +90,9 @@ class UrlValidator(StrictDictValidator):
|
|
|
89
90
|
validator=OverridesBooleanFormatterValidator,
|
|
90
91
|
default="False",
|
|
91
92
|
)
|
|
93
|
+
self._webpage_url = self._validate_key(
|
|
94
|
+
key="webpage_url", validator=StringFormatterValidator, default="{webpage_url}"
|
|
95
|
+
)
|
|
92
96
|
|
|
93
97
|
@property
|
|
94
98
|
def url(self) -> OverridesStringFormatterValidator:
|
|
@@ -180,6 +184,19 @@ class UrlValidator(StrictDictValidator):
|
|
|
180
184
|
"""
|
|
181
185
|
return self._include_sibling_metadata
|
|
182
186
|
|
|
187
|
+
@property
|
|
188
|
+
def webpage_url(self) -> StringFormatterValidator:
|
|
189
|
+
"""
|
|
190
|
+
Optional. After ytdl-sub performs the metadata download, it will inspect each
|
|
191
|
+
entry's .info.json file and perform the actual download from yt-dlp using
|
|
192
|
+
`webpage_url <config_reference/scripting/entry_variables:webpage_url>`. This
|
|
193
|
+
can be overwritten by supplying parameter with a modification to ``webpage_url`` in the
|
|
194
|
+
form of an override variable.
|
|
195
|
+
|
|
196
|
+
Defaults to ``{webpage_url}``.
|
|
197
|
+
"""
|
|
198
|
+
return self._webpage_url
|
|
199
|
+
|
|
183
200
|
|
|
184
201
|
class UrlStringOrDictValidator(UrlValidator):
|
|
185
202
|
"""
|
ytdl_sub/plugins/subtitles.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import os
|
|
1
2
|
from pathlib import Path
|
|
2
3
|
from typing import Dict
|
|
3
4
|
from typing import List
|
|
4
5
|
from typing import Optional
|
|
5
6
|
from typing import Set
|
|
7
|
+
from typing import Tuple
|
|
6
8
|
|
|
7
9
|
from ytdl_sub.config.plugin.plugin import Plugin
|
|
8
10
|
from ytdl_sub.config.plugin.plugin_operation import PluginOperation
|
|
@@ -11,6 +13,7 @@ from ytdl_sub.downloaders.ytdl_options_builder import YTDLOptionsBuilder
|
|
|
11
13
|
from ytdl_sub.entries.entry import Entry
|
|
12
14
|
from ytdl_sub.entries.script.variable_definitions import VARIABLES
|
|
13
15
|
from ytdl_sub.entries.script.variable_definitions import VariableDefinitions
|
|
16
|
+
from ytdl_sub.script.utils.exceptions import UserThrownRuntimeError
|
|
14
17
|
from ytdl_sub.utils.file_handler import FileHandler
|
|
15
18
|
from ytdl_sub.utils.file_handler import FileMetadata
|
|
16
19
|
from ytdl_sub.utils.logger import Logger
|
|
@@ -57,6 +60,7 @@ class SubtitleOptions(ToggleableOptionsDictValidator):
|
|
|
57
60
|
"subtitles_type",
|
|
58
61
|
"embed_subtitles",
|
|
59
62
|
"languages",
|
|
63
|
+
"languages_required",
|
|
60
64
|
"allow_auto_generated_subtitles",
|
|
61
65
|
}
|
|
62
66
|
|
|
@@ -76,6 +80,9 @@ class SubtitleOptions(ToggleableOptionsDictValidator):
|
|
|
76
80
|
self._languages = self._validate_key_if_present(
|
|
77
81
|
key="languages", validator=StringListValidator, default=["en"]
|
|
78
82
|
).list
|
|
83
|
+
self._languages_required = self._validate_key_if_present(
|
|
84
|
+
key="languages_required", validator=StringListValidator, default=[]
|
|
85
|
+
).list
|
|
79
86
|
self._allow_auto_generated_subtitles = self._validate_key_if_present(
|
|
80
87
|
key="allow_auto_generated_subtitles", validator=BoolValidator, default=False
|
|
81
88
|
).value
|
|
@@ -121,6 +128,16 @@ class SubtitleOptions(ToggleableOptionsDictValidator):
|
|
|
121
128
|
"""
|
|
122
129
|
return [lang.value for lang in self._languages]
|
|
123
130
|
|
|
131
|
+
@property
|
|
132
|
+
def languages_required(self) -> Optional[List[str]]:
|
|
133
|
+
"""
|
|
134
|
+
:expected type: Optional[List[String]]
|
|
135
|
+
:description:
|
|
136
|
+
Language code(s) that are required to be present for downloads to continue. If missing,
|
|
137
|
+
ytdl-sub will throw an error. NOTE: currently this only checks file-based subtitles.
|
|
138
|
+
"""
|
|
139
|
+
return [lang.value for lang in self._languages_required]
|
|
140
|
+
|
|
124
141
|
@property
|
|
125
142
|
def allow_auto_generated_subtitles(self) -> Optional[bool]:
|
|
126
143
|
"""
|
|
@@ -211,7 +228,21 @@ class SubtitlesPlugin(Plugin[SubtitleOptions]):
|
|
|
211
228
|
if self.plugin_options.embed_subtitles:
|
|
212
229
|
file_metadata = FileMetadata(f"Embedded subtitles with lang(s) {', '.join(langs)}")
|
|
213
230
|
if self.plugin_options.subtitles_name:
|
|
231
|
+
download_subtitle_lang_file_names: List[Tuple[str, str]] = []
|
|
232
|
+
|
|
214
233
|
for lang in langs:
|
|
234
|
+
download_subtitle_file_name = entry.base_filename(
|
|
235
|
+
ext=f"{lang}.{self.plugin_options.subtitles_type}"
|
|
236
|
+
)
|
|
237
|
+
if os.path.isfile(Path(self.working_directory) / download_subtitle_file_name):
|
|
238
|
+
download_subtitle_lang_file_names.append((lang, download_subtitle_file_name))
|
|
239
|
+
elif lang in self.plugin_options.languages_required:
|
|
240
|
+
raise UserThrownRuntimeError(
|
|
241
|
+
f"Required the subtitle lang {lang}, but the file could not be found for "
|
|
242
|
+
f"the entry {entry.title}."
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
for lang, file_name in download_subtitle_lang_file_names:
|
|
215
246
|
output_subtitle_file_name = self.overrides.apply_formatter(
|
|
216
247
|
formatter=self.plugin_options.subtitles_name,
|
|
217
248
|
entry=entry,
|
|
@@ -219,9 +250,7 @@ class SubtitlesPlugin(Plugin[SubtitleOptions]):
|
|
|
219
250
|
)
|
|
220
251
|
|
|
221
252
|
self.save_file(
|
|
222
|
-
file_name=
|
|
223
|
-
ext=f"{lang}.{self.plugin_options.subtitles_type}"
|
|
224
|
-
),
|
|
253
|
+
file_name=file_name,
|
|
225
254
|
output_file_name=output_subtitle_file_name,
|
|
226
255
|
entry=entry,
|
|
227
256
|
)
|