feat(template): add configurable folder naming via output_template.folder (#94)

Adds an optional `folder` key under `output_template` to customize output folder names using the same template variables as file naming.
This commit is contained in:
Andy
2026-03-25 21:42:47 -06:00
parent 10cca7d0ea
commit e323f6f3b3
6 changed files with 80 additions and 10 deletions

View File

@@ -61,6 +61,32 @@ Example outputs:
- Scene series: `Example.Show.2024.S01E01.Pilot.1080p.EXAMPLE.WEB-DL.DDP5.1.H.264-TAG`
- Plex movies: `Example Movie (2024) 1080p`
### folder (optional)
Controls the folder name for downloaded content. Uses the same template variables as the file templates above.
If not configured, the default folder naming is used:
- Movies: `Title (Year)`
- Series: Derived from the `series` template with episode-specific variables removed
- Songs: `Artist - Album (Year)`
```yaml
output_template:
movies: '{title}.{year}.{repack?}.{edition?}.{quality}.{source}.WEB-DL.{dual?}.{multi?}.{audio_full}.{atmos?}.{hdr?}.{hfr?}.{video}-{tag}'
series: '{title}.{year?}.{season_episode}.{episode_name?}.{repack?}.{edition?}.{quality}.{source}.WEB-DL.{dual?}.{multi?}.{audio_full}.{atmos?}.{hdr?}.{hfr?}.{video}-{tag}'
songs: '{track_number}.{title}.{repack?}.{edition?}.{source?}.WEB-DL.{audio_full}.{atmos?}-{tag}'
# Scene-style folder
folder: '{title}.{year?}.{repack?}.{edition?}.{lang_tag?}.{quality}.{source}.WEB-DL.{dual?}.{multi?}.{audio_full}.{atmos?}.{hdr?}.{hfr?}.{video}-{tag}'
# Plex-friendly folder
# folder: '{title} ({year?})'
```
Example outputs:
- Scene folder: `Example.Show.2024.S01.1080p.EXAMPLE.WEB-DL.DDP5.1.H.264-TAG/`
- Plex folder: `Example Show (2024)/`
---
---

View File

@@ -99,6 +99,7 @@ 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 ""
if kwargs.get("scene_naming") is not None:
raise SystemExit(
@@ -161,7 +162,11 @@ class Config:
unsafe_chars = r'[<>:"/\\|?*]'
for template_type, template_str in self.output_template.items():
all_templates = dict(self.output_template)
if self.folder_template:
all_templates["folder"] = self.folder_template
for template_type, template_str in all_templates.items():
if not isinstance(template_str, str):
warnings.warn(f"Template '{template_type}' must be a string, got {type(template_str).__name__}")
continue

View File

@@ -100,19 +100,31 @@ 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)
context = self._build_template_context(media_info, show_service)
context['season'] = f"S{self.season:02}"
folder_name = formatter.format(context)
if '.' in config.folder_template and ' ' not in config.folder_template:
return sanitize_filename(folder_name, ".")
else:
return sanitize_filename(folder_name, " ")
series_template = config.output_template.get("series")
if series_template:
folder_template = series_template
folder_template = re.sub(r'\{episode\}', '', folder_template)
folder_template = re.sub(r'\{episode_name\?\}', '', folder_template)
folder_template = re.sub(r'\{episode_name\}', '', folder_template)
folder_template = re.sub(r'\{season_episode\}', '{season}', folder_template)
derived_template = series_template
derived_template = re.sub(r'\{episode\}', '', derived_template)
derived_template = re.sub(r'\{episode_name\?\}', '', derived_template)
derived_template = re.sub(r'\{episode_name\}', '', derived_template)
derived_template = re.sub(r'\{season_episode\}', '{season}', derived_template)
folder_template = re.sub(r'\.{2,}', '.', folder_template)
folder_template = re.sub(r'\s{2,}', ' ', folder_template)
folder_template = re.sub(r'^[\.\s]+|[\.\s]+$', '', folder_template)
derived_template = re.sub(r'\.{2,}', '.', derived_template)
derived_template = re.sub(r'\s{2,}', ' ', derived_template)
derived_template = re.sub(r'^[\.\s]+|[\.\s]+$', '', derived_template)
formatter = TemplateFormatter(folder_template)
formatter = TemplateFormatter(derived_template)
context = self._build_template_context(media_info, show_service)
context['season'] = f"S{self.season:02}"

View File

@@ -59,6 +59,14 @@ 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)
context = self._build_template_context(media_info, show_service)
folder_name = formatter.format(context)
if '.' in config.folder_template and ' ' not in config.folder_template:
return sanitize_filename(folder_name, ".")
else:
return sanitize_filename(folder_name, " ")
name = f"{self.name}"
if self.year:
name += f" ({self.year})"

View File

@@ -94,6 +94,14 @@ 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)
context = self._build_template_context(media_info, show_service)
folder_name = formatter.format(context)
if '.' in config.folder_template and ' ' not in config.folder_template:
return sanitize_filename(folder_name, ".")
else:
return sanitize_filename(folder_name, " ")
name = f"{self.artist} - {self.album}"
if self.year:
name += f" ({self.year})"

View File

@@ -52,6 +52,17 @@ output_template:
# Custom scene-style with specific elements
# movies: '{title}.{year}.{quality}.{hdr?}.{source}.WEB-DL.{audio_full}.{video}-{tag}'
# series: '{title}.{year?}.{season_episode}.{episode_name?}.{quality}.{hdr?}.{source}.WEB-DL.{audio_full}.{atmos?}.{video}-{tag}'
#
# Folder naming (optional). Controls the folder name for downloaded content.
# If not configured, series folders are derived from the series template (minus episode info),
# movie folders use "{title} ({year})", and song folders use "{artist} - {album} ({year})".
# Uses the same template variables as the file templates above.
#
# Scene-style folder:
# folder: '{title}.{year?}.{repack?}.{edition?}.{lang_tag?}.{quality}.{source}.WEB-DL.{dual?}.{multi?}.{audio_full}.{atmos?}.{hdr?}.{hfr?}.{video}-{tag}'
#
# Plex-friendly folder:
# folder: '{title} ({year?})'
# Language-based tagging for output filenames
# Automatically adds language identifiers (e.g., DANiSH, NORDiC, DKsubs) based on