diff --git a/unshackle/commands/dl.py b/unshackle/commands/dl.py index 2bf08c8..f3b4955 100644 --- a/unshackle/commands/dl.py +++ b/unshackle/commands/dl.py @@ -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) diff --git a/unshackle/core/titles/episode.py b/unshackle/core/titles/episode.py index cdf94d2..58a0aff 100644 --- a/unshackle/core/titles/episode.py +++ b/unshackle/core/titles/episode.py @@ -123,46 +123,71 @@ 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}" + 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$$ + + # Add year if configured + if self.year and config.series_year: + name += f" {self.year}" + + # Add season and episode + name += f" - S{self.season:02}E{self.number:02}" + + # Add episode name with dash separator + if self.name: + name += f" - {self.name}" + + name = name.strip() else: - if config.dash_naming: - # Format: Title - SXXEXX - Episode Name - name = self.title.replace("$", "S") # e.g., Arli$$ - - # Add year if configured - if self.year and config.series_year: - name += f" {self.year}" - - # Add season and episode - name += f" - S{self.season:02}E{self.number:02}" - - # Add episode name with dash separator - if self.name: - name += f" - {self.name}" - - name = name.strip() - else: - # Standard format without extra dashes - name = "{title}{year} S{season:02}E{number:02} {name}".format( - title=self.title.replace("$", "S"), # e.g., Arli$$ - year=f" {self.year}" if self.year and config.series_year else "", - season=self.season, - number=self.number, - name=self.name or "", - ).strip() + # Standard format without extra dashes + name = "{title}{year} S{season:02}E{number:02} {name}".format( + title=self.title.replace("$", "S"), # e.g., Arli$$ + year=f" {self.year}" if self.year and config.series_year else "", + season=self.season, + number=self.number, + name=self.name or "", + ).strip() if primary_video_track: resolution_token = _get_resolution_token(primary_video_track) if resolution_token: - name += f" {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,15 +196,22 @@ class Episode(Title): name += f" {source_name or self.service.__name__}" # 'WEB-DL' - name += " WEB-DL" + if config.scene_naming: + name += " WEB-DL" # DUAL if unique_audio_languages == 2: - name += " DUAL" + if non_scene_episode_file: + _append_unique_token(extra_tokens, "DUAL") + else: + name += " DUAL" # MULTi if unique_audio_languages > 2: - name += " MULTi" + if non_scene_episode_file: + _append_unique_token(extra_tokens, "MULTi") + else: + name += " MULTi" # Audio Codec + Channels (+ feature) if primary_audio_track: @@ -194,9 +226,16 @@ 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: - name += " Atmos" + if non_scene_episode_file: + _append_unique_token(extra_tokens, "Atmos") + else: + name += " Atmos" # Video (dynamic range + hfr +) Codec if primary_video_track: @@ -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"): - name = _append_token(name, "DV") + 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"] ): - name = _append_token(name, "HDR") + if non_scene_episode_file: + _append_unique_token(extra_tokens, "HDR") + else: + name = _append_token(name, "HDR") elif "HDR Vivid" in hdr_format: - name = _append_token(name, "HDR") + 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 "" - name = _append_token(name, dynamic_range) + 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(): - name += " HLG" + 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(): - name += " HDR" + if non_scene_episode_file: + _append_unique_token(extra_tokens, "HDR") + else: + name += " HDR" if frame_rate > 30: - name += " HFR" - name += f" {VIDEO_CODEC_MAP.get(codec, codec)}" + if non_scene_episode_file: + _append_unique_token(extra_tokens, "HFR") + else: + name += " HFR" + 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}"