diff --git a/unshackle/core/config.py b/unshackle/core/config.py index 06bf787..f80497a 100644 --- a/unshackle/core/config.py +++ b/unshackle/core/config.py @@ -98,7 +98,13 @@ class Config: self.language_tags: dict = kwargs.get("language_tags") or {} self.output_template: dict = kwargs.get("output_template") or {} - self.folder_template: str = self.output_template.pop("folder", "") or "" + folder_cfg = self.output_template.pop("folder", "") + self.folder_template: str = "" + self.folder_templates: dict = {} + if isinstance(folder_cfg, dict): + self.folder_templates = {k: v for k, v in folder_cfg.items() if isinstance(v, str) and v} + elif isinstance(folder_cfg, str): + self.folder_template = folder_cfg or "" if kwargs.get("scene_naming") is not None: raise SystemExit( @@ -158,6 +164,11 @@ class Config: all_templates = dict(self.output_template) if self.folder_template: all_templates["folder"] = self.folder_template + for kind, tmpl in self.folder_templates.items(): + if kind not in {"movies", "series", "songs"}: + warnings.warn(f"Unknown folder template kind '{kind}' (expected movies/series/songs)") + continue + all_templates[f"folder.{kind}"] = tmpl for template_type, template_str in all_templates.items(): if not isinstance(template_str, str): @@ -178,6 +189,18 @@ class Config: if not template_str.strip(): warnings.warn(f"Template '{template_type}' is empty") + def get_folder_template(self, kind: str) -> str: + """Resolve the folder template for the given title kind. + + kind: one of "movies", "series", "songs". + Falls back to the legacy single-string folder template, then "". + """ + if self.folder_templates: + tmpl = self.folder_templates.get(kind) + if tmpl: + return tmpl + return self.folder_template or "" + def get_template_separator(self, template_type: str = "movies") -> str: """Get the filename separator for the given template type. diff --git a/unshackle/core/titles/episode.py b/unshackle/core/titles/episode.py index 020f369..fa4b9d2 100644 --- a/unshackle/core/titles/episode.py +++ b/unshackle/core/titles/episode.py @@ -100,14 +100,15 @@ class Episode(Title): def get_filename(self, media_info: MediaInfo, folder: bool = False, show_service: bool = True) -> str: if folder: - if config.folder_template: - formatter = TemplateFormatter(config.folder_template) + template = config.get_folder_template("series") + if template: + formatter = TemplateFormatter(template) context = self._build_template_context(media_info, show_service) context["season"] = f"S{self.season:02}" folder_name = formatter.format(context) - separators = re.sub(r"\{[^}]*\}", "", config.folder_template) + separators = re.sub(r"\{[^}]*\}", "", template) spacer = "." if "." in separators and " " not in separators else " " return sanitize_filename(folder_name, spacer) diff --git a/unshackle/core/titles/movie.py b/unshackle/core/titles/movie.py index fcde150..c611493 100644 --- a/unshackle/core/titles/movie.py +++ b/unshackle/core/titles/movie.py @@ -60,12 +60,13 @@ class Movie(Title): def get_filename(self, media_info: MediaInfo, folder: bool = False, show_service: bool = True) -> str: if folder: - if config.folder_template: - formatter = TemplateFormatter(config.folder_template) + template = config.get_folder_template("movies") + if template: + formatter = TemplateFormatter(template) context = self._build_template_context(media_info, show_service) folder_name = formatter.format(context) - separators = re.sub(r"\{[^}]*\}", "", config.folder_template) + separators = re.sub(r"\{[^}]*\}", "", template) spacer = "." if "." in separators and " " not in separators else " " return sanitize_filename(folder_name, spacer) name = f"{self.name}" diff --git a/unshackle/core/titles/song.py b/unshackle/core/titles/song.py index 402d5a6..465b659 100644 --- a/unshackle/core/titles/song.py +++ b/unshackle/core/titles/song.py @@ -95,12 +95,13 @@ class Song(Title): def get_filename(self, media_info: MediaInfo, folder: bool = False, show_service: bool = True) -> str: if folder: - if config.folder_template: - formatter = TemplateFormatter(config.folder_template) + template = config.get_folder_template("songs") + if template: + formatter = TemplateFormatter(template) context = self._build_template_context(media_info, show_service) folder_name = formatter.format(context) - separators = re.sub(r"\{[^}]*\}", "", config.folder_template) + separators = re.sub(r"\{[^}]*\}", "", template) spacer = "." if "." in separators and " " not in separators else " " return sanitize_filename(folder_name, spacer) name = f"{self.artist} - {self.album}" diff --git a/unshackle/unshackle-example.yaml b/unshackle/unshackle-example.yaml index 63b4352..6cd2b8c 100644 --- a/unshackle/unshackle-example.yaml +++ b/unshackle/unshackle-example.yaml @@ -63,6 +63,16 @@ output_template: # # Plex-friendly folder: # folder: '{title} ({year?})' + # + # Per-title-type folder templates (optional). Override folder naming separately for + # movies, series, and songs. Useful when music libraries need artist/album-style folders + # while movies/series follow a different scheme. Any kind omitted falls back to the + # default for that title type. + # + # folder: + # movies: '{title} ({year})' + # series: '{title} ({year?})' + # songs: '{artist}/{album} ({year?})' # Language-based tagging for output filenames # Automatically adds language identifiers (e.g., DANiSH, NORDiC, DKsubs) based on