forked from kenzuya/unshackle
Compare commits
2 Commits
3e45f3efe7
...
0a820e6552
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a820e6552 | |||
| 8748ce8a11 |
@@ -60,7 +60,7 @@ 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.hybrid import Hybrid
|
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,
|
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 import tags
|
||||||
from unshackle.core.utils.click_types import (AUDIO_CODEC_LIST, LANGUAGE_RANGE, QUALITY_LIST, SEASON_RANGE,
|
from unshackle.core.utils.click_types import (AUDIO_CODEC_LIST, LANGUAGE_RANGE, QUALITY_LIST, SEASON_RANGE,
|
||||||
ContextData, MultipleChoice, SubtitleCodecChoice, VideoCodecChoice)
|
ContextData, MultipleChoice, SubtitleCodecChoice, VideoCodecChoice)
|
||||||
@@ -2015,6 +2015,8 @@ class dl:
|
|||||||
|
|
||||||
sidecar_dir = config.directories.downloads
|
sidecar_dir = config.directories.downloads
|
||||||
if not no_folder and isinstance(title, (Episode, Song)) and media_info:
|
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 /= title.get_filename(media_info, show_service=not no_source, folder=True)
|
||||||
sidecar_dir.mkdir(parents=True, exist_ok=True)
|
sidecar_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
@@ -2080,6 +2082,8 @@ class dl:
|
|||||||
)
|
)
|
||||||
if sample_track and sample_track.path:
|
if sample_track and sample_track.path:
|
||||||
media_info = MediaInfo.parse(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 /= title.get_filename(media_info, show_service=not no_source, folder=True)
|
||||||
|
|
||||||
final_dir.mkdir(parents=True, exist_ok=True)
|
final_dir.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -2122,6 +2126,8 @@ class dl:
|
|||||||
audio_codec_suffix = muxed_audio_codecs.get(muxed_path)
|
audio_codec_suffix = muxed_audio_codecs.get(muxed_path)
|
||||||
|
|
||||||
if not no_folder and isinstance(title, (Episode, Song)):
|
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 /= title.get_filename(media_info, show_service=not no_source, folder=True)
|
||||||
|
|
||||||
final_dir.mkdir(parents=True, exist_ok=True)
|
final_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|||||||
@@ -123,46 +123,71 @@ class Episode(Title):
|
|||||||
scan_suffix = "i"
|
scan_suffix = "i"
|
||||||
return f"{resolution}{scan_suffix}"
|
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)
|
# Title [Year] SXXEXX Name (or Title [Year] SXX if folder)
|
||||||
if folder:
|
if folder:
|
||||||
name = f"{self.title}"
|
name = f"{self.title}"
|
||||||
if self.year and config.series_year:
|
if self.year and config.series_year:
|
||||||
name += f" {self.year}"
|
name += f" {self.year}"
|
||||||
name += f" S{self.season:02}"
|
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:
|
else:
|
||||||
if config.dash_naming:
|
# Standard format without extra dashes
|
||||||
# Format: Title - SXXEXX - Episode Name
|
name = "{title}{year} S{season:02}E{number:02} {name}".format(
|
||||||
name = self.title.replace("$", "S") # e.g., Arli$$
|
title=self.title.replace("$", "S"), # e.g., Arli$$
|
||||||
|
year=f" {self.year}" if self.year and config.series_year else "",
|
||||||
# Add year if configured
|
season=self.season,
|
||||||
if self.year and config.series_year:
|
number=self.number,
|
||||||
name += f" {self.year}"
|
name=self.name or "",
|
||||||
|
).strip()
|
||||||
# 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()
|
|
||||||
|
|
||||||
if primary_video_track:
|
if primary_video_track:
|
||||||
resolution_token = _get_resolution_token(primary_video_track)
|
resolution_token = _get_resolution_token(primary_video_track)
|
||||||
if resolution_token:
|
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)
|
# Service (use track source if available)
|
||||||
if show_service:
|
if show_service and config.scene_naming:
|
||||||
source_name = None
|
source_name = None
|
||||||
if self.tracks:
|
if self.tracks:
|
||||||
first_track = next(iter(self.tracks), None)
|
first_track = next(iter(self.tracks), None)
|
||||||
@@ -171,15 +196,22 @@ class Episode(Title):
|
|||||||
name += f" {source_name or self.service.__name__}"
|
name += f" {source_name or self.service.__name__}"
|
||||||
|
|
||||||
# 'WEB-DL'
|
# 'WEB-DL'
|
||||||
name += " WEB-DL"
|
if config.scene_naming:
|
||||||
|
name += " WEB-DL"
|
||||||
|
|
||||||
# DUAL
|
# DUAL
|
||||||
if unique_audio_languages == 2:
|
if unique_audio_languages == 2:
|
||||||
name += " DUAL"
|
if non_scene_episode_file:
|
||||||
|
_append_unique_token(extra_tokens, "DUAL")
|
||||||
|
else:
|
||||||
|
name += " DUAL"
|
||||||
|
|
||||||
# MULTi
|
# MULTi
|
||||||
if unique_audio_languages > 2:
|
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)
|
# Audio Codec + Channels (+ feature)
|
||||||
if primary_audio_track:
|
if primary_audio_track:
|
||||||
@@ -194,9 +226,16 @@ class Episode(Title):
|
|||||||
channels = float(channel_count)
|
channels = float(channel_count)
|
||||||
|
|
||||||
features = primary_audio_track.format_additionalfeatures or ""
|
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 "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
|
# Video (dynamic range + hfr +) Codec
|
||||||
if primary_video_track:
|
if primary_video_track:
|
||||||
@@ -210,36 +249,55 @@ class Episode(Title):
|
|||||||
)
|
)
|
||||||
frame_rate = float(primary_video_track.frame_rate)
|
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
|
# Primary HDR format detection
|
||||||
if hdr_format:
|
if hdr_format:
|
||||||
if hdr_format_full.startswith("Dolby Vision"):
|
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(
|
if any(
|
||||||
indicator in (hdr_format_full + " " + hdr_format)
|
indicator in (hdr_format_full + " " + hdr_format)
|
||||||
for indicator in ["HDR10", "SMPTE ST 2086"]
|
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:
|
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:
|
else:
|
||||||
dynamic_range = DYNAMIC_RANGE_MAP.get(hdr_format) or hdr_format or ""
|
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():
|
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():
|
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:
|
if frame_rate > 30:
|
||||||
name += " HFR"
|
if non_scene_episode_file:
|
||||||
name += f" {VIDEO_CODEC_MAP.get(codec, codec)}"
|
_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:
|
if config.tag:
|
||||||
name += f"-{config.tag}"
|
name += f"-{config.tag}"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import re
|
||||||
|
import unicodedata
|
||||||
from abc import ABC
|
from abc import ABC
|
||||||
from typing import Any, Iterable, Optional, Union
|
from typing import Any, Iterable, Optional, Union
|
||||||
|
|
||||||
@@ -5,6 +7,7 @@ from langcodes import Language
|
|||||||
from pymediainfo import MediaInfo
|
from pymediainfo import MediaInfo
|
||||||
from rich.tree import Tree
|
from rich.tree import Tree
|
||||||
from sortedcontainers import SortedKeyList
|
from sortedcontainers import SortedKeyList
|
||||||
|
from unidecode import unidecode
|
||||||
|
|
||||||
from unshackle.core.config import config
|
from unshackle.core.config import config
|
||||||
from unshackle.core.constants import AUDIO_CODEC_MAP, DYNAMIC_RANGE_MAP, VIDEO_CODEC_MAP
|
from unshackle.core.constants import AUDIO_CODEC_MAP, DYNAMIC_RANGE_MAP, VIDEO_CODEC_MAP
|
||||||
@@ -52,6 +55,18 @@ class Movie(Title):
|
|||||||
return f"{self.name} ({self.year})"
|
return f"{self.name} ({self.year})"
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _sanitize_non_scene_filename(filename: str) -> str:
|
||||||
|
if not config.unicode_filenames:
|
||||||
|
filename = unidecode(filename)
|
||||||
|
|
||||||
|
filename = "".join(c for c in filename if unicodedata.category(c) != "Mn")
|
||||||
|
filename = filename.replace("/", " & ").replace(";", " & ")
|
||||||
|
filename = re.sub(r"[\\:*!?¿,'\"<>|$#~]", "", filename)
|
||||||
|
filename = re.sub(r"\s{2,}", " ", filename)
|
||||||
|
|
||||||
|
return filename.strip()
|
||||||
|
|
||||||
def get_filename(self, media_info: MediaInfo, folder: bool = False, show_service: bool = True) -> str:
|
def get_filename(self, media_info: MediaInfo, folder: bool = False, show_service: bool = True) -> str:
|
||||||
primary_video_track = next(iter(media_info.video_tracks), None)
|
primary_video_track = next(iter(media_info.video_tracks), None)
|
||||||
primary_audio_track = None
|
primary_audio_track = None
|
||||||
@@ -89,7 +104,11 @@ class Movie(Title):
|
|||||||
return f"{resolution}{scan_suffix}"
|
return f"{resolution}{scan_suffix}"
|
||||||
|
|
||||||
# Name (Year)
|
# Name (Year)
|
||||||
name = str(self).replace("$", "S") # e.g., Arli$$
|
name = self.name.replace("$", "S") # e.g., Arli$$
|
||||||
|
if self.year:
|
||||||
|
name = f"{name} ({self.year})"
|
||||||
|
if not config.scene_naming:
|
||||||
|
name += " -"
|
||||||
|
|
||||||
if primary_video_track:
|
if primary_video_track:
|
||||||
resolution_token = _get_resolution_token(primary_video_track)
|
resolution_token = _get_resolution_token(primary_video_track)
|
||||||
@@ -179,7 +198,9 @@ class Movie(Title):
|
|||||||
if config.tag:
|
if config.tag:
|
||||||
name += f"-{config.tag}"
|
name += f"-{config.tag}"
|
||||||
|
|
||||||
return sanitize_filename(name, "." if config.scene_naming else " ")
|
if config.scene_naming:
|
||||||
|
return sanitize_filename(name, ".")
|
||||||
|
return self._sanitize_non_scene_filename(name)
|
||||||
|
|
||||||
|
|
||||||
class Movies(SortedKeyList, ABC):
|
class Movies(SortedKeyList, ABC):
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Group or Username to postfix to the end of all download filenames following a dash
|
# Group or Username to postfix to the end of all download filenames following a dash
|
||||||
tag: Kenzuya
|
# tag: Kenzuya
|
||||||
|
|
||||||
# Enable/disable tagging with group name (default: true)
|
# Enable/disable tagging with group name (default: true)
|
||||||
tag_group_name: true
|
tag_group_name: true
|
||||||
@@ -13,7 +13,7 @@ set_terminal_bg: false
|
|||||||
# Set file naming convention
|
# Set file naming convention
|
||||||
# true for style - Prime.Suspect.S07E01.The.Final.Act.Part.One.1080p.ITV.WEB-DL.AAC2.0.H.264
|
# true for style - Prime.Suspect.S07E01.The.Final.Act.Part.One.1080p.ITV.WEB-DL.AAC2.0.H.264
|
||||||
# false for style - Prime Suspect S07E01 The Final Act - Part One
|
# false for style - Prime Suspect S07E01 The Final Act - Part One
|
||||||
scene_naming: true
|
scene_naming: false
|
||||||
|
|
||||||
# Whether to include the year in series names for episodes and folders (default: true)
|
# Whether to include the year in series names for episodes and folders (default: true)
|
||||||
# true for style - Show Name (2023) S01E01 Episode Name
|
# true for style - Show Name (2023) S01E01 Episode Name
|
||||||
@@ -64,7 +64,7 @@ directories:
|
|||||||
cache: Cache
|
cache: Cache
|
||||||
# cookies: Cookies
|
# cookies: Cookies
|
||||||
dcsl: DCSL # Device Certificate Status List
|
dcsl: DCSL # Device Certificate Status List
|
||||||
downloads: /home/kenzuya/Mounts/ketuakenzuya/Downloads
|
downloads: Downloads
|
||||||
logs: Logs
|
logs: Logs
|
||||||
temp: Temp
|
temp: Temp
|
||||||
# wvds: WVDs
|
# wvds: WVDs
|
||||||
|
|||||||
Reference in New Issue
Block a user