fix(dl): prevent missing tracks error when excluding track types

- Define keep_* flags early in the track selection loop to accurately trace which track types (video, audio, subs, chapters) should be kept.
- Guard specific track filtering and validation blocks in dl.py using keep_* variables.
- Prevent fatal 'not found in video/audio/subtitle tracks' errors when using skip options like --no-video (-nv) or --no-subs (-ns).
- Simplify and clean up the trailing track cleanup logic to rely on the pre-determined keep_* flags.
This commit is contained in:
CodeName393
2026-07-12 19:07:27 +09:00
committed by GitHub
parent f42af571a7
commit ccb2a19e7a
+132 -108
View File
@@ -47,8 +47,16 @@ from unshackle.core.constants import DOWNLOAD_CANCELLED, DOWNLOAD_LICENCE_ONLY,
from unshackle.core.credential import Credential from unshackle.core.credential import Credential
from unshackle.core.drm import DRM_T, ClearKeyCENC, MonaLisa, PlayReady, Widevine from unshackle.core.drm import DRM_T, ClearKeyCENC, MonaLisa, PlayReady, Widevine
from unshackle.core.events import events from unshackle.core.events import events
from unshackle.core.music import (MusicAudioIntegrityError, MusicMetadataResult, MusicPlanner, MusicRenderer, from unshackle.core.music import (
file_md5, verify_music_audio, write_music_manifest, write_music_metadata) MusicAudioIntegrityError,
MusicMetadataResult,
MusicPlanner,
MusicRenderer,
file_md5,
verify_music_audio,
write_music_manifest,
write_music_metadata,
)
from unshackle.core.proxies import Basic, ExpressVPN, Gluetun, Hola, NordVPN, ProtonVPN, SurfsharkVPN, WindscribeVPN from unshackle.core.proxies import Basic, ExpressVPN, Gluetun, Hola, NordVPN, ProtonVPN, SurfsharkVPN, WindscribeVPN
from unshackle.core.service import Service from unshackle.core.service import Service
from unshackle.core.services import Services from unshackle.core.services import Services
@@ -59,14 +67,30 @@ from unshackle.core.tracks import Audio, Subtitle, Tracks, Video
from unshackle.core.tracks.attachment import Attachment from unshackle.core.tracks.attachment import Attachment
from unshackle.core.tracks.dv_fixup import apply_dv_fixup from unshackle.core.tracks.dv_fixup import apply_dv_fixup
from unshackle.core.tracks.hybrid import Hybrid from unshackle.core.tracks.hybrid import Hybrid
from unshackle.core.utilities import (find_font_with_fallbacks, find_missing_langs, get_debug_logger, from unshackle.core.utilities import (
get_system_fonts, init_debug_logger, is_close_match, log_event, find_font_with_fallbacks,
suggest_font_packages, time_elapsed_since) find_missing_langs,
get_debug_logger,
get_system_fonts,
init_debug_logger,
is_close_match,
log_event,
suggest_font_packages,
time_elapsed_since,
)
from unshackle.core.utils import tags from unshackle.core.utils import tags
from unshackle.core.utils.bitrate import apply_real_bitrates from unshackle.core.utils.bitrate import apply_real_bitrates
from unshackle.core.utils.click_types import (AUDIO_CODEC_LIST, LANGUAGE_RANGE, QUALITY_LIST, SEASON_RANGE, from unshackle.core.utils.click_types import (
SLOW_DELAY_RANGE, ContextData, MultipleChoice, MultipleVideoCodecChoice, AUDIO_CODEC_LIST,
SubtitleCodecChoice) LANGUAGE_RANGE,
QUALITY_LIST,
SEASON_RANGE,
SLOW_DELAY_RANGE,
ContextData,
MultipleChoice,
MultipleVideoCodecChoice,
SubtitleCodecChoice,
)
from unshackle.core.utils.collections import ci_get, merge_dict from unshackle.core.utils.collections import ci_get, merge_dict
from unshackle.core.utils.selector import select_multiple from unshackle.core.utils.selector import select_multiple
from unshackle.core.utils.subprocess import ffprobe from unshackle.core.utils.subprocess import ffprobe
@@ -842,25 +866,41 @@ class dl:
if name == "shaka_packager": if name == "shaka_packager":
r = subprocess.run( r = subprocess.run(
[str(binary), "--version"], [str(binary), "--version"],
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=5, capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=5,
) )
version = (r.stdout or r.stderr or "").strip() version = (r.stdout or r.stderr or "").strip()
elif name in ("ffmpeg", "ffprobe"): elif name in ("ffmpeg", "ffprobe"):
r = subprocess.run( r = subprocess.run(
[str(binary), "-version"], [str(binary), "-version"],
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=5, capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=5,
) )
version = (r.stdout or "").split("\n")[0].strip() version = (r.stdout or "").split("\n")[0].strip()
elif name == "mkvmerge": elif name == "mkvmerge":
r = subprocess.run( r = subprocess.run(
[str(binary), "--version"], [str(binary), "--version"],
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=5, capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=5,
) )
version = (r.stdout or "").strip() version = (r.stdout or "").strip()
elif name == "mp4decrypt": elif name == "mp4decrypt":
r = subprocess.run( r = subprocess.run(
[str(binary)], [str(binary)],
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=5, capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=5,
) )
output = (r.stdout or "") + (r.stderr or "") output = (r.stdout or "") + (r.stderr or "")
lines = [line.strip() for line in output.split("\n") if line.strip()] lines = [line.strip() for line in output.split("\n") if line.strip()]
@@ -1303,7 +1343,9 @@ class dl:
else: else:
vaults_only = not cdm_only vaults_only = not cdm_only
config.decryption = resolve_decryption(config.decryption_map, config.decryption, service.__class__.__name__.upper()) config.decryption = resolve_decryption(
config.decryption_map, config.decryption, service.__class__.__name__.upper()
)
log_event( log_event(
"drm_mode_config", "drm_mode_config",
@@ -2134,32 +2176,8 @@ class dl:
events.subscribe(events.Types.TRACK_REPACKED, service.on_track_repacked) events.subscribe(events.Types.TRACK_REPACKED, service.on_track_repacked)
events.subscribe(events.Types.TRACK_MULTIPLEX, service.on_track_multiplex) events.subscribe(events.Types.TRACK_MULTIPLEX, service.on_track_multiplex)
if hasattr(service, "NO_SUBTITLES") and service.NO_SUBTITLES: # The no_xxx guards are handled later right before track selection.
console.log("Skipping subtitles - service does not support subtitle downloads") pass
no_subs = True
s_lang = None
title.tracks.subtitles = []
elif no_subs:
console.log("Skipped subtitles as --no-subs was used...")
s_lang = None
title.tracks.subtitles = []
if no_video:
console.log("Skipped video as --no-video was used...")
v_lang = None
title.tracks.videos = []
if no_audio:
console.log("Skipped audio as --no-audio was used...")
a_lang = None
title.tracks.audio = []
if no_chapters:
console.log("Skipped chapters as --no-chapters was used...")
title.tracks.chapters = []
if no_proxy_download and any(service.session.proxies.values()):
console.log("Bypassing proxy for downloads as --no-proxy-download was used...")
tracks_label = "Getting Remote Tracks..." if self.is_remote else "Getting Tracks..." tracks_label = "Getting Remote Tracks..." if self.is_remote else "Getting Tracks..."
with console.status(tracks_label, spinner="dots"): with console.status(tracks_label, spinner="dots"):
@@ -2292,10 +2310,58 @@ class dl:
console.print(Padding(Panel(available_tracks, title="Available Tracks"), (0, 5))) console.print(Padding(Panel(available_tracks, title="Available Tracks"), (0, 5)))
continue continue
# Determine which tracks to keep
keep_videos = True
keep_audio = True
keep_subtitles = True
keep_chapters = True
if video_only or audio_only or subs_only or chapters_only:
keep_videos = video_only
keep_audio = audio_only
keep_subtitles = subs_only
keep_chapters = chapters_only
if hasattr(service, "NO_SUBTITLES") and service.NO_SUBTITLES:
console.log("Skipping subtitles - service does not support subtitle downloads")
keep_subtitles = False
s_lang = None
elif no_subs:
console.log("Skipped subtitles as --no-subs was used...")
keep_subtitles = False
s_lang = None
if no_video:
console.log("Skipped video as --no-video was used...")
keep_videos = False
v_lang = None
if no_audio:
console.log("Skipped audio as --no-audio was used...")
keep_audio = False
a_lang = None
if no_chapters:
console.log("Skipped chapters as --no-chapters was used...")
keep_chapters = False
if no_proxy_download and any(service.session.proxies.values()):
console.log("Bypassing proxy for downloads as --no-proxy-download was used...")
# Clear unwanted tracks
if not keep_videos:
title.tracks.videos = []
if not keep_audio:
title.tracks.audio = []
if not keep_subtitles:
title.tracks.subtitles = []
if not keep_chapters:
title.tracks.chapters = []
with console.status("Selecting tracks...", spinner="dots"): with console.status("Selecting tracks...", spinner="dots"):
if isinstance(title, (Movie, Episode)): if isinstance(title, (Movie, Episode)):
# filter video tracks # filter video tracks
if vcodec: if keep_videos and vcodec:
title.tracks.select_video(lambda x: x.codec in vcodec) title.tracks.select_video(lambda x: x.codec in vcodec)
missing_codecs = [c for c in vcodec if not any(x.codec == c for x in title.tracks.videos)] missing_codecs = [c for c in vcodec if not any(x.codec == c for x in title.tracks.videos)]
for codec in missing_codecs: for codec in missing_codecs:
@@ -2304,7 +2370,7 @@ class dl:
self.log.error(f"There's no {', '.join(c.name for c in vcodec)} Video Track...") self.log.error(f"There's no {', '.join(c.name for c in vcodec)} Video Track...")
sys.exit(1) sys.exit(1)
if range_: if keep_videos and range_:
# Special handling for HYBRID - don't filter, keep all HDR10 and DV tracks # Special handling for HYBRID - don't filter, keep all HDR10 and DV tracks
if Video.Range.HYBRID not in range_: if Video.Range.HYBRID not in range_:
title.tracks.select_video(lambda x: x.range in range_) title.tracks.select_video(lambda x: x.range in range_)
@@ -2315,7 +2381,7 @@ class dl:
self.log.error(f"There's no {', '.join(r.name for r in range_)} Video Track...") self.log.error(f"There's no {', '.join(r.name for r in range_)} Video Track...")
sys.exit(1) sys.exit(1)
if vbitrate: if keep_videos and vbitrate:
if any(r == Video.Range.HYBRID for r in range_): if any(r == Video.Range.HYBRID for r in range_):
# In HYBRID mode, only apply bitrate filter to non-DV tracks # In HYBRID mode, only apply bitrate filter to non-DV tracks
# DV tracks are kept regardless since they're only used for RPU metadata # DV tracks are kept regardless since they're only used for RPU metadata
@@ -2331,7 +2397,7 @@ class dl:
self.log.error(f"There's no {vbitrate}kbps Video Track...") self.log.error(f"There's no {vbitrate}kbps Video Track...")
sys.exit(1) sys.exit(1)
if vbitrate_min is not None and vbitrate_max is not None: if keep_videos and vbitrate_min is not None and vbitrate_max is not None:
title.tracks.select_video( title.tracks.select_video(
lambda x: x.bitrate and vbitrate_min <= x.bitrate // 1000 <= vbitrate_max lambda x: x.bitrate and vbitrate_min <= x.bitrate // 1000 <= vbitrate_max
) )
@@ -2344,7 +2410,7 @@ class dl:
video_multi_lang = ( video_multi_lang = (
"best" in effective_video_lang or "all" in effective_video_lang or len(video_languages) > 1 "best" in effective_video_lang or "all" in effective_video_lang or len(video_languages) > 1
) )
if video_languages and "all" not in video_languages: if keep_videos and video_languages and "all" not in video_languages:
processed_video_lang = [] processed_video_lang = []
for language in video_languages: for language in video_languages:
if language == "orig": if language == "orig":
@@ -2367,9 +2433,7 @@ class dl:
if missing_v_langs: if missing_v_langs:
missing_str = ", ".join(missing_v_langs) missing_str = ", ".join(missing_v_langs)
if best_available: if best_available:
remaining_v_langs = [ remaining_v_langs = [tok for tok in processed_video_lang if tok not in missing_v_langs]
tok for tok in processed_video_lang if tok not in missing_v_langs
]
if remaining_v_langs: if remaining_v_langs:
self.log.warning( self.log.warning(
f"{missing_str} not found in video tracks, " f"{missing_str} not found in video tracks, "
@@ -2377,9 +2441,7 @@ class dl:
) )
processed_video_lang = remaining_v_langs processed_video_lang = remaining_v_langs
else: else:
self.log.error( self.log.error(f"{missing_str} not found in video tracks and no fallback available")
f"{missing_str} not found in video tracks and no fallback available"
)
sys.exit(1) sys.exit(1)
else: else:
self.log.error(missing_str + " not found in video tracks") self.log.error(missing_str + " not found in video tracks")
@@ -2394,7 +2456,7 @@ class dl:
has_hybrid = any(r == Video.Range.HYBRID for r in range_) has_hybrid = any(r == Video.Range.HYBRID for r in range_)
non_hybrid_ranges = [r for r in range_ if r != Video.Range.HYBRID] non_hybrid_ranges = [r for r in range_ if r != Video.Range.HYBRID]
effective_quality = quality effective_quality = quality
if quality: if keep_videos and quality:
missing_resolutions = [] missing_resolutions = []
if has_hybrid: if has_hybrid:
hybrid_candidate_tracks, non_hybrid_tracks = Tracks.partition_hybrid_videos( hybrid_candidate_tracks, non_hybrid_tracks = Tracks.partition_hybrid_videos(
@@ -2468,7 +2530,7 @@ class dl:
# choose best track by range and quality # choose best track by range and quality
pre_hybrid_videos: list[Video] = list(title.tracks.videos) if has_hybrid else [] pre_hybrid_videos: list[Video] = list(title.tracks.videos) if has_hybrid else []
if has_hybrid: if keep_videos and has_hybrid:
# Apply hybrid selection for HYBRID tracks # Apply hybrid selection for HYBRID tracks
hybrid_candidate_tracks, non_hybrid_tracks = Tracks.partition_hybrid_videos( hybrid_candidate_tracks, non_hybrid_tracks = Tracks.partition_hybrid_videos(
title.tracks.videos, non_hybrid_ranges title.tracks.videos, non_hybrid_ranges
@@ -2522,7 +2584,7 @@ class dl:
# the lowest DV) so the standalone mux loop skips them. Tracks also # the lowest DV) so the standalone mux loop skips them. Tracks also
# picked as explicit deliverables stay unflagged. # picked as explicit deliverables stay unflagged.
Tracks.flag_hybrid_ingredients(hybrid_selected, non_hybrid_selected) Tracks.flag_hybrid_ingredients(hybrid_selected, non_hybrid_selected)
else: elif keep_videos:
selected_videos: list[Video] = [] selected_videos: list[Video] = []
if video_multi_lang: if video_multi_lang:
unique_video_langs = list(dict.fromkeys(str(v.language) for v in title.tracks.videos)) unique_video_langs = list(dict.fromkeys(str(v.language) for v in title.tracks.videos))
@@ -2549,7 +2611,7 @@ class dl:
title.tracks.videos = selected_videos title.tracks.videos = selected_videos
# validate hybrid mode requirements # validate hybrid mode requirements
if any(r == Video.Range.HYBRID for r in range_): if keep_videos and any(r == Video.Range.HYBRID for r in range_):
base_tracks = [ base_tracks = [
v for v in title.tracks.videos if v.range in (Video.Range.HDR10, Video.Range.HDR10P) v for v in title.tracks.videos if v.range in (Video.Range.HDR10, Video.Range.HDR10P)
] ]
@@ -2613,7 +2675,7 @@ class dl:
sys.exit(1) sys.exit(1)
# filter subtitle tracks # filter subtitle tracks
if require_subs: if keep_subtitles and require_subs:
missing_langs = [ missing_langs = [
lang lang
for lang in require_subs for lang in require_subs
@@ -2627,7 +2689,7 @@ class dl:
self.log.info( self.log.info(
f"Required languages found ({', '.join(require_subs)}), downloading all available subtitles" f"Required languages found ({', '.join(require_subs)}), downloading all available subtitles"
) )
elif s_lang and "all" not in s_lang: elif keep_subtitles and s_lang and "all" not in s_lang:
from unshackle.core.utilities import is_exact_match from unshackle.core.utilities import is_exact_match
match_func = is_exact_match if exact_lang else is_close_match match_func = is_exact_match if exact_lang else is_close_match
@@ -2661,12 +2723,12 @@ class dl:
self.log.error(f"There's no {s_lang} Subtitle Track...") self.log.error(f"There's no {s_lang} Subtitle Track...")
sys.exit(1) sys.exit(1)
if not forced_subs: if keep_subtitles and not forced_subs:
title.tracks.select_subtitles(lambda x: not x.forced) title.tracks.select_subtitles(lambda x: not x.forced)
# filter audio tracks # filter audio tracks
# might have no audio tracks if part of the video, e.g. transport stream hls # might have no audio tracks if part of the video, e.g. transport stream hls
if len(title.tracks.audio) > 0: if keep_audio and len(title.tracks.audio) > 0:
if not audio_description: if not audio_description:
title.tracks.select_audio(lambda x: not x.descriptive) # exclude descriptive audio title.tracks.select_audio(lambda x: not x.descriptive) # exclude descriptive audio
if acodec: if acodec:
@@ -2747,57 +2809,19 @@ class dl:
self.log.error(f"There's no {processed_lang} Audio Track, cannot continue...") self.log.error(f"There's no {processed_lang} Audio Track, cannot continue...")
sys.exit(1) sys.exit(1)
if ( # Reconstruct track set to only include kept tracks
video_only kept_tracks = []
or audio_only if keep_videos:
or subs_only kept_tracks.extend(title.tracks.videos)
or chapters_only if keep_audio:
or no_subs kept_tracks.extend(title.tracks.audio)
or no_audio if keep_subtitles:
or no_chapters kept_tracks.extend(title.tracks.subtitles)
or no_video if keep_chapters:
): kept_tracks.extend(title.tracks.chapters)
keep_videos = False kept_tracks.extend(title.tracks.attachments)
keep_audio = False
keep_subtitles = False
keep_chapters = False
if video_only or audio_only or subs_only or chapters_only: title.tracks = Tracks(kept_tracks, manifest_url=title.tracks.manifest_url)
if video_only:
keep_videos = True
if audio_only:
keep_audio = True
if subs_only:
keep_subtitles = True
if chapters_only:
keep_chapters = True
else:
keep_videos = True
keep_audio = True
keep_subtitles = True
keep_chapters = True
if no_subs:
keep_subtitles = False
if no_audio:
keep_audio = False
if no_chapters:
keep_chapters = False
if no_video:
keep_videos = False
kept_tracks = []
if keep_videos:
kept_tracks.extend(title.tracks.videos)
if keep_audio:
kept_tracks.extend(title.tracks.audio)
if keep_subtitles:
kept_tracks.extend(title.tracks.subtitles)
if keep_chapters:
kept_tracks.extend(title.tracks.chapters)
kept_tracks.extend(title.tracks.attachments)
title.tracks = Tracks(kept_tracks, manifest_url=title.tracks.manifest_url)
selected_tracks, tracks_progress_callables = title.tracks.tree(add_progress=True) selected_tracks, tracks_progress_callables = title.tracks.tree(add_progress=True)