feat(movie): improve non-scene filename sanitization

Add dedicated non-scene filename sanitization for movies to produce cleaner, filesystem-safe names when `scene_naming` is disabled. The new logic:
- optionally transliterates unicode based on `unicode_filenames`
- removes combining marks/diacritics and disallowed punctuation
- normalizes separators and extra whitespace

Also update movie name construction to explicitly format `Name (Year)` and append a trailing ` -` for non-scene naming before metadata tokens, improving readability and consistency.

Update default config values in `unshackle.yaml` to match non-scene/local usage:
- disable `scene_naming` by default
- comment out default `tag`
- change downloads directory to `Downloads`feat(movie): improve non-scene filename sanitization

Add dedicated non-scene filename sanitization for movies to produce cleaner, filesystem-safe names when `scene_naming` is disabled. The new logic:
- optionally transliterates unicode based on `unicode_filenames`
- removes combining marks/diacritics and disallowed punctuation
- normalizes separators and extra whitespace

Also update movie name construction to explicitly format `Name (Year)` and append a trailing ` -` for non-scene naming before metadata tokens, improving readability and consistency.

Update default config values in `unshackle.yaml` to match non-scene/local usage:
- disable `scene_naming` by default
- comment out default `tag`
- change downloads directory to `Downloads`
This commit is contained in:
kenzuya
2026-03-02 19:42:53 +07:00
parent 3e45f3efe7
commit 8748ce8a11
2 changed files with 26 additions and 5 deletions

View File

@@ -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):

View File

@@ -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