forked from kenzuya/unshackle
fix(dl): normalize non-scene episode folder/file naming
Ensure episode downloads use a consistent non-scene layout when `scene_naming` is disabled by: - adding a sanitized series-title directory before season/episode folders for sidecars, sample-based paths, and muxed outputs - updating `Episode.get_filename()` to return `Season XX` for folder names in non-scene mode - generating non-scene episode file names as `Title SXXEXX - Episode Name` - adding token append helpers to avoid duplicate/empty naming tokens This keeps output paths predictable across download stages and prevents naming inconsistencies or duplicated suffixes.fix(dl): normalize non-scene episode folder/file naming Ensure episode downloads use a consistent non-scene layout when `scene_naming` is disabled by: - adding a sanitized series-title directory before season/episode folders for sidecars, sample-based paths, and muxed outputs - updating `Episode.get_filename()` to return `Season XX` for folder names in non-scene mode - generating non-scene episode file names as `Title SXXEXX - Episode Name` - adding token append helpers to avoid duplicate/empty naming tokens This keeps output paths predictable across download stages and prevents naming inconsistencies or duplicated suffixes.
This commit is contained in:
@@ -60,7 +60,7 @@ from unshackle.core.tracks import Audio, Subtitle, Tracks, Video
|
||||
from unshackle.core.tracks.attachment import Attachment
|
||||
from unshackle.core.tracks.hybrid import Hybrid
|
||||
from unshackle.core.utilities import (find_font_with_fallbacks, get_debug_logger, get_system_fonts, init_debug_logger,
|
||||
is_close_match, suggest_font_packages, time_elapsed_since)
|
||||
is_close_match, sanitize_filename, suggest_font_packages, time_elapsed_since)
|
||||
from unshackle.core.utils import tags
|
||||
from unshackle.core.utils.click_types import (AUDIO_CODEC_LIST, LANGUAGE_RANGE, QUALITY_LIST, SEASON_RANGE,
|
||||
ContextData, MultipleChoice, SubtitleCodecChoice, VideoCodecChoice)
|
||||
@@ -2015,6 +2015,8 @@ class dl:
|
||||
|
||||
sidecar_dir = config.directories.downloads
|
||||
if not no_folder and isinstance(title, (Episode, Song)) and media_info:
|
||||
if isinstance(title, Episode) and not config.scene_naming:
|
||||
sidecar_dir /= sanitize_filename(title.title.replace("$", "S"), " ")
|
||||
sidecar_dir /= title.get_filename(media_info, show_service=not no_source, folder=True)
|
||||
sidecar_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -2080,6 +2082,8 @@ class dl:
|
||||
)
|
||||
if sample_track and sample_track.path:
|
||||
media_info = MediaInfo.parse(sample_track.path)
|
||||
if isinstance(title, Episode) and not config.scene_naming:
|
||||
final_dir /= sanitize_filename(title.title.replace("$", "S"), " ")
|
||||
final_dir /= title.get_filename(media_info, show_service=not no_source, folder=True)
|
||||
|
||||
final_dir.mkdir(parents=True, exist_ok=True)
|
||||
@@ -2122,6 +2126,8 @@ class dl:
|
||||
audio_codec_suffix = muxed_audio_codecs.get(muxed_path)
|
||||
|
||||
if not no_folder and isinstance(title, (Episode, Song)):
|
||||
if isinstance(title, Episode) and not config.scene_naming:
|
||||
final_dir /= sanitize_filename(title.title.replace("$", "S"), " ")
|
||||
final_dir /= title.get_filename(media_info, show_service=not no_source, folder=True)
|
||||
|
||||
final_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -123,14 +123,36 @@ class Episode(Title):
|
||||
scan_suffix = "i"
|
||||
return f"{resolution}{scan_suffix}"
|
||||
|
||||
def _append_token(current: str, token: Optional[str]) -> str:
|
||||
token = (token or "").strip()
|
||||
current = current.rstrip()
|
||||
if not token:
|
||||
return current
|
||||
if current.endswith(f" {token}"):
|
||||
return current
|
||||
return f"{current} {token}"
|
||||
|
||||
def _append_unique_token(tokens: list[str], token: Optional[str]) -> None:
|
||||
token = (token or "").strip()
|
||||
if token and token not in tokens:
|
||||
tokens.append(token)
|
||||
|
||||
if folder and not config.scene_naming:
|
||||
return sanitize_filename(f"Season {self.season:02}", " ")
|
||||
|
||||
non_scene_episode_file = not config.scene_naming and not folder
|
||||
extra_tokens: list[str] = []
|
||||
|
||||
# Title [Year] SXXEXX Name (or Title [Year] SXX if folder)
|
||||
if folder:
|
||||
name = f"{self.title}"
|
||||
if self.year and config.series_year:
|
||||
name += f" {self.year}"
|
||||
name += f" S{self.season:02}"
|
||||
else:
|
||||
if config.dash_naming:
|
||||
elif non_scene_episode_file:
|
||||
episode_label = self.name or f"Episode {self.number:02}"
|
||||
name = f"{self.title.replace('$', 'S')} S{self.season:02}E{self.number:02} - {episode_label}"
|
||||
elif config.dash_naming:
|
||||
# Format: Title - SXXEXX - Episode Name
|
||||
name = self.title.replace("$", "S") # e.g., Arli$$
|
||||
|
||||
@@ -159,10 +181,13 @@ class Episode(Title):
|
||||
if primary_video_track:
|
||||
resolution_token = _get_resolution_token(primary_video_track)
|
||||
if resolution_token:
|
||||
if non_scene_episode_file:
|
||||
_append_unique_token(extra_tokens, resolution_token)
|
||||
else:
|
||||
name += f" {resolution_token}"
|
||||
|
||||
# Service (use track source if available)
|
||||
if show_service:
|
||||
if show_service and config.scene_naming:
|
||||
source_name = None
|
||||
if self.tracks:
|
||||
first_track = next(iter(self.tracks), None)
|
||||
@@ -171,14 +196,21 @@ class Episode(Title):
|
||||
name += f" {source_name or self.service.__name__}"
|
||||
|
||||
# 'WEB-DL'
|
||||
if config.scene_naming:
|
||||
name += " WEB-DL"
|
||||
|
||||
# DUAL
|
||||
if unique_audio_languages == 2:
|
||||
if non_scene_episode_file:
|
||||
_append_unique_token(extra_tokens, "DUAL")
|
||||
else:
|
||||
name += " DUAL"
|
||||
|
||||
# MULTi
|
||||
if unique_audio_languages > 2:
|
||||
if non_scene_episode_file:
|
||||
_append_unique_token(extra_tokens, "MULTi")
|
||||
else:
|
||||
name += " MULTi"
|
||||
|
||||
# Audio Codec + Channels (+ feature)
|
||||
@@ -194,8 +226,15 @@ class Episode(Title):
|
||||
channels = float(channel_count)
|
||||
|
||||
features = primary_audio_track.format_additionalfeatures or ""
|
||||
name += f" {AUDIO_CODEC_MAP.get(codec, codec)}{channels:.1f}"
|
||||
audio_token = f"{AUDIO_CODEC_MAP.get(codec, codec)}{channels:.1f}"
|
||||
if non_scene_episode_file:
|
||||
_append_unique_token(extra_tokens, audio_token)
|
||||
else:
|
||||
name += f" {audio_token}"
|
||||
if "JOC" in features or primary_audio_track.joc:
|
||||
if non_scene_episode_file:
|
||||
_append_unique_token(extra_tokens, "Atmos")
|
||||
else:
|
||||
name += " Atmos"
|
||||
|
||||
# Video (dynamic range + hfr +) Codec
|
||||
@@ -210,36 +249,55 @@ class Episode(Title):
|
||||
)
|
||||
frame_rate = float(primary_video_track.frame_rate)
|
||||
|
||||
def _append_token(current: str, token: Optional[str]) -> str:
|
||||
token = (token or "").strip()
|
||||
current = current.rstrip()
|
||||
if not token:
|
||||
return current
|
||||
if current.endswith(f" {token}"):
|
||||
return current
|
||||
return f"{current} {token}"
|
||||
|
||||
# Primary HDR format detection
|
||||
if hdr_format:
|
||||
if hdr_format_full.startswith("Dolby Vision"):
|
||||
if non_scene_episode_file:
|
||||
_append_unique_token(extra_tokens, "DV")
|
||||
else:
|
||||
name = _append_token(name, "DV")
|
||||
if any(
|
||||
indicator in (hdr_format_full + " " + hdr_format)
|
||||
for indicator in ["HDR10", "SMPTE ST 2086"]
|
||||
):
|
||||
if non_scene_episode_file:
|
||||
_append_unique_token(extra_tokens, "HDR")
|
||||
else:
|
||||
name = _append_token(name, "HDR")
|
||||
elif "HDR Vivid" in hdr_format:
|
||||
if non_scene_episode_file:
|
||||
_append_unique_token(extra_tokens, "HDR")
|
||||
else:
|
||||
name = _append_token(name, "HDR")
|
||||
else:
|
||||
dynamic_range = DYNAMIC_RANGE_MAP.get(hdr_format) or hdr_format or ""
|
||||
if non_scene_episode_file:
|
||||
_append_unique_token(extra_tokens, dynamic_range)
|
||||
else:
|
||||
name = _append_token(name, dynamic_range)
|
||||
elif "HLG" in trc or "Hybrid Log-Gamma" in trc or "ARIB STD-B67" in trc or "arib-std-b67" in trc.lower():
|
||||
if non_scene_episode_file:
|
||||
_append_unique_token(extra_tokens, "HLG")
|
||||
else:
|
||||
name += " HLG"
|
||||
elif any(indicator in trc for indicator in ["PQ", "SMPTE ST 2084", "BT.2100"]) or "smpte2084" in trc.lower() or "bt.2020-10" in trc.lower():
|
||||
if non_scene_episode_file:
|
||||
_append_unique_token(extra_tokens, "HDR")
|
||||
else:
|
||||
name += " HDR"
|
||||
if frame_rate > 30:
|
||||
if non_scene_episode_file:
|
||||
_append_unique_token(extra_tokens, "HFR")
|
||||
else:
|
||||
name += " HFR"
|
||||
name += f" {VIDEO_CODEC_MAP.get(codec, codec)}"
|
||||
video_codec_token = VIDEO_CODEC_MAP.get(codec, codec)
|
||||
if non_scene_episode_file:
|
||||
_append_unique_token(extra_tokens, video_codec_token)
|
||||
else:
|
||||
name += f" {video_codec_token}"
|
||||
|
||||
if non_scene_episode_file and extra_tokens:
|
||||
name += f" - {' '.join(extra_tokens)}"
|
||||
|
||||
if config.tag:
|
||||
name += f"-{config.tag}"
|
||||
|
||||
Reference in New Issue
Block a user