mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-05-16 21:59:26 +00:00
feat(config): per-title-type folder templates
Allow output_template.folder to be a dict with movies/series/songs keys so music libraries can use artist/album folder layouts while movies and series keep their own scheme. Legacy string form still applies to all title types.
This commit is contained in:
@@ -98,7 +98,13 @@ class Config:
|
|||||||
|
|
||||||
self.language_tags: dict = kwargs.get("language_tags") or {}
|
self.language_tags: dict = kwargs.get("language_tags") or {}
|
||||||
self.output_template: dict = kwargs.get("output_template") 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:
|
if kwargs.get("scene_naming") is not None:
|
||||||
raise SystemExit(
|
raise SystemExit(
|
||||||
@@ -158,6 +164,11 @@ class Config:
|
|||||||
all_templates = dict(self.output_template)
|
all_templates = dict(self.output_template)
|
||||||
if self.folder_template:
|
if self.folder_template:
|
||||||
all_templates["folder"] = 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():
|
for template_type, template_str in all_templates.items():
|
||||||
if not isinstance(template_str, str):
|
if not isinstance(template_str, str):
|
||||||
@@ -178,6 +189,18 @@ class Config:
|
|||||||
if not template_str.strip():
|
if not template_str.strip():
|
||||||
warnings.warn(f"Template '{template_type}' is empty")
|
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:
|
def get_template_separator(self, template_type: str = "movies") -> str:
|
||||||
"""Get the filename separator for the given template type.
|
"""Get the filename separator for the given template type.
|
||||||
|
|
||||||
|
|||||||
@@ -100,14 +100,15 @@ class Episode(Title):
|
|||||||
|
|
||||||
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:
|
||||||
if folder:
|
if folder:
|
||||||
if config.folder_template:
|
template = config.get_folder_template("series")
|
||||||
formatter = TemplateFormatter(config.folder_template)
|
if template:
|
||||||
|
formatter = TemplateFormatter(template)
|
||||||
context = self._build_template_context(media_info, show_service)
|
context = self._build_template_context(media_info, show_service)
|
||||||
context["season"] = f"S{self.season:02}"
|
context["season"] = f"S{self.season:02}"
|
||||||
|
|
||||||
folder_name = formatter.format(context)
|
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 " "
|
spacer = "." if "." in separators and " " not in separators else " "
|
||||||
return sanitize_filename(folder_name, spacer)
|
return sanitize_filename(folder_name, spacer)
|
||||||
|
|
||||||
|
|||||||
@@ -60,12 +60,13 @@ class Movie(Title):
|
|||||||
|
|
||||||
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:
|
||||||
if folder:
|
if folder:
|
||||||
if config.folder_template:
|
template = config.get_folder_template("movies")
|
||||||
formatter = TemplateFormatter(config.folder_template)
|
if template:
|
||||||
|
formatter = TemplateFormatter(template)
|
||||||
context = self._build_template_context(media_info, show_service)
|
context = self._build_template_context(media_info, show_service)
|
||||||
folder_name = formatter.format(context)
|
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 " "
|
spacer = "." if "." in separators and " " not in separators else " "
|
||||||
return sanitize_filename(folder_name, spacer)
|
return sanitize_filename(folder_name, spacer)
|
||||||
name = f"{self.name}"
|
name = f"{self.name}"
|
||||||
|
|||||||
@@ -95,12 +95,13 @@ class Song(Title):
|
|||||||
|
|
||||||
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:
|
||||||
if folder:
|
if folder:
|
||||||
if config.folder_template:
|
template = config.get_folder_template("songs")
|
||||||
formatter = TemplateFormatter(config.folder_template)
|
if template:
|
||||||
|
formatter = TemplateFormatter(template)
|
||||||
context = self._build_template_context(media_info, show_service)
|
context = self._build_template_context(media_info, show_service)
|
||||||
folder_name = formatter.format(context)
|
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 " "
|
spacer = "." if "." in separators and " " not in separators else " "
|
||||||
return sanitize_filename(folder_name, spacer)
|
return sanitize_filename(folder_name, spacer)
|
||||||
name = f"{self.artist} - {self.album}"
|
name = f"{self.artist} - {self.album}"
|
||||||
|
|||||||
@@ -63,6 +63,16 @@ output_template:
|
|||||||
#
|
#
|
||||||
# Plex-friendly folder:
|
# Plex-friendly folder:
|
||||||
# folder: '{title} ({year?})'
|
# 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
|
# Language-based tagging for output filenames
|
||||||
# Automatically adds language identifiers (e.g., DANiSH, NORDiC, DKsubs) based on
|
# Automatically adds language identifiers (e.g., DANiSH, NORDiC, DKsubs) based on
|
||||||
|
|||||||
Reference in New Issue
Block a user