mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-05-17 06:09:29 +00:00
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:
@@ -61,6 +61,32 @@ Example outputs:
|
|||||||
- Scene series: `Example.Show.2024.S01E01.Pilot.1080p.EXAMPLE.WEB-DL.DDP5.1.H.264-TAG`
|
- Scene series: `Example.Show.2024.S01E01.Pilot.1080p.EXAMPLE.WEB-DL.DDP5.1.H.264-TAG`
|
||||||
- Plex movies: `Example Movie (2024) 1080p`
|
- 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)/`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ 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 ""
|
||||||
|
|
||||||
if kwargs.get("scene_naming") is not None:
|
if kwargs.get("scene_naming") is not None:
|
||||||
raise SystemExit(
|
raise SystemExit(
|
||||||
@@ -161,7 +162,11 @@ class Config:
|
|||||||
|
|
||||||
unsafe_chars = r'[<>:"/\\|?*]'
|
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):
|
if not isinstance(template_str, str):
|
||||||
warnings.warn(f"Template '{template_type}' must be a string, got {type(template_str).__name__}")
|
warnings.warn(f"Template '{template_type}' must be a string, got {type(template_str).__name__}")
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -100,19 +100,31 @@ 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:
|
||||||
|
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")
|
series_template = config.output_template.get("series")
|
||||||
if series_template:
|
if series_template:
|
||||||
folder_template = series_template
|
derived_template = series_template
|
||||||
folder_template = re.sub(r'\{episode\}', '', folder_template)
|
derived_template = re.sub(r'\{episode\}', '', derived_template)
|
||||||
folder_template = re.sub(r'\{episode_name\?\}', '', folder_template)
|
derived_template = re.sub(r'\{episode_name\?\}', '', derived_template)
|
||||||
folder_template = re.sub(r'\{episode_name\}', '', folder_template)
|
derived_template = re.sub(r'\{episode_name\}', '', derived_template)
|
||||||
folder_template = re.sub(r'\{season_episode\}', '{season}', folder_template)
|
derived_template = re.sub(r'\{season_episode\}', '{season}', derived_template)
|
||||||
|
|
||||||
folder_template = re.sub(r'\.{2,}', '.', folder_template)
|
derived_template = re.sub(r'\.{2,}', '.', derived_template)
|
||||||
folder_template = re.sub(r'\s{2,}', ' ', folder_template)
|
derived_template = re.sub(r'\s{2,}', ' ', derived_template)
|
||||||
folder_template = re.sub(r'^[\.\s]+|[\.\s]+$', '', folder_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 = self._build_template_context(media_info, show_service)
|
||||||
context['season'] = f"S{self.season:02}"
|
context['season'] = f"S{self.season:02}"
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,14 @@ 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:
|
||||||
|
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}"
|
name = f"{self.name}"
|
||||||
if self.year:
|
if self.year:
|
||||||
name += f" ({self.year})"
|
name += f" ({self.year})"
|
||||||
|
|||||||
@@ -94,6 +94,14 @@ 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:
|
||||||
|
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}"
|
name = f"{self.artist} - {self.album}"
|
||||||
if self.year:
|
if self.year:
|
||||||
name += f" ({self.year})"
|
name += f" ({self.year})"
|
||||||
|
|||||||
@@ -52,6 +52,17 @@ output_template:
|
|||||||
# Custom scene-style with specific elements
|
# Custom scene-style with specific elements
|
||||||
# movies: '{title}.{year}.{quality}.{hdr?}.{source}.WEB-DL.{audio_full}.{video}-{tag}'
|
# 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}'
|
# 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
|
# 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