mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-06-10 03:02:09 +00:00
feat(naming): per-service title_map remapping (#106)
Add services.<TAG>.title_map exact-match dict to rewrite service-provided titles before naming/output. Shared remap_titles helper applied on local dl, import, and client-side dl --remote (server stays raw so clients can override names for services they don't have installed locally).
This commit is contained in:
@@ -27,7 +27,7 @@ EXAMPLE:
|
||||
|
||||
You can override many global configuration options on a per-service basis by nesting them under the
|
||||
service tag in the `services` section. Supported override keys include: `dl`, `subtitle`, `muxing`,
|
||||
`headers`, `proxy_map`, and more.
|
||||
`headers`, `proxy_map`, `title_map`, and more.
|
||||
|
||||
Overrides are merged with global config (not replaced) -- only specified keys are overridden, others
|
||||
use global defaults. CLI arguments always take priority over service-specific config.
|
||||
@@ -47,6 +47,29 @@ services:
|
||||
Note: unshackle uses a single unified `requests`-based downloader. The legacy `aria2c`,
|
||||
`n_m3u8dl_re`, and `curl_impersonate` override sections have been removed.
|
||||
|
||||
### title_map (dict)
|
||||
|
||||
Rewrites service-provided titles before naming and output. Some services name a title differently
|
||||
from how you want it stored, which can break library matching (e.g. a regional variant reusing the
|
||||
international name). Keys are the exact title string the service returns; values are the desired
|
||||
output title.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
EXAMPLE:
|
||||
title_map:
|
||||
Service Title: Desired Title
|
||||
```
|
||||
|
||||
Episodes are matched on their show title, Movies and Songs on their name. The remap is applied
|
||||
after the title cache (so edits take effect without a cache reset) and before any `--enrich`
|
||||
override (so an explicit enrich still wins).
|
||||
|
||||
It applies on the local `dl` path, the `import` command, and the remote client (`dl --remote`).
|
||||
For remote services the **client's** `title_map` is applied to the titles returned by the server,
|
||||
so you can rename titles for services you don't have installed locally. The server sends raw
|
||||
titles and does not remap, leaving the final name fully under the client's control.
|
||||
|
||||
### Service Class Conventions
|
||||
|
||||
Each service directory under `unshackle/services/` exports a class extending
|
||||
|
||||
@@ -16,7 +16,7 @@ from unshackle.core.credential import Credential
|
||||
from unshackle.core.drm import drm_from_dict
|
||||
from unshackle.core.manifests import DASH, HLS, ISM
|
||||
from unshackle.core.remote_service import RemoteService, _build_title, _resolve_proxy
|
||||
from unshackle.core.titles import Episode, Movies, Series, Title_T, Titles_T
|
||||
from unshackle.core.titles import Episode, Movies, Series, Title_T, Titles_T, remap_titles
|
||||
from unshackle.core.tracks import Audio, Chapter, Chapters, Tracks, Video
|
||||
from unshackle.core.tracks.track import Track
|
||||
|
||||
@@ -123,7 +123,9 @@ class ImportService:
|
||||
return self.titles
|
||||
|
||||
def get_titles_cached(self, title_id: Optional[str] = None) -> Titles_T:
|
||||
return self.get_titles()
|
||||
"""Apply the service's title_map to titles reconstructed from the export sidecar."""
|
||||
title_map = (config.services.get(self.service_tag) or {}).get("title_map") or {}
|
||||
return remap_titles(self.get_titles(), title_map)
|
||||
|
||||
def get_tracks(self, title: Title_T) -> Tracks:
|
||||
"""Reconstruct the title's tracks from the export.
|
||||
|
||||
@@ -25,7 +25,7 @@ from unshackle.core.config import config
|
||||
from unshackle.core.console import console
|
||||
from unshackle.core.constants import AnyTrack
|
||||
from unshackle.core.credential import Credential
|
||||
from unshackle.core.titles import Title_T, Titles_T
|
||||
from unshackle.core.titles import Title_T, Titles_T, remap_titles
|
||||
from unshackle.core.titles.episode import Episode, Series
|
||||
from unshackle.core.titles.movie import Movie, Movies
|
||||
from unshackle.core.tracks import Audio, Chapter, Chapters, Subtitle, Tracks, Video
|
||||
@@ -573,7 +573,13 @@ class RemoteService:
|
||||
return self._titles
|
||||
|
||||
def get_titles_cached(self, title_id: str = None) -> Titles_T:
|
||||
return self.get_titles()
|
||||
"""Apply the client's local title_map to titles fetched from the remote server.
|
||||
|
||||
Lets users rename titles for remote services they don't have installed locally.
|
||||
The server sends raw titles; the client's own ``services.<TAG>.title_map`` wins.
|
||||
"""
|
||||
title_map = (config.services.get(self.service_tag) or {}).get("title_map") or {}
|
||||
return remap_titles(self.get_titles(), title_map)
|
||||
|
||||
def get_tracks(self, title: Title_T) -> Tracks:
|
||||
title_id = str(title.id)
|
||||
|
||||
@@ -27,7 +27,7 @@ from unshackle.core.credential import Credential
|
||||
from unshackle.core.drm import DRM_T
|
||||
from unshackle.core.search_result import SearchResult
|
||||
from unshackle.core.title_cacher import TitleCacher, get_account_hash, get_region_from_proxy
|
||||
from unshackle.core.titles import Title_T, Titles_T
|
||||
from unshackle.core.titles import Title_T, Titles_T, remap_titles
|
||||
from unshackle.core.tracks import Chapters, Tracks
|
||||
from unshackle.core.tracks.video import Video
|
||||
from unshackle.core.utils.ip_info import get_ip_info
|
||||
@@ -482,7 +482,7 @@ class Service(metaclass=ABCMeta):
|
||||
else:
|
||||
# If we can't determine title_id, just call get_titles directly
|
||||
self.log.debug("Cannot determine title_id for caching, bypassing cache")
|
||||
return self.get_titles()
|
||||
return self.apply_title_map(self.get_titles())
|
||||
|
||||
# Get cache control flags from context
|
||||
no_cache = False
|
||||
@@ -495,7 +495,7 @@ class Service(metaclass=ABCMeta):
|
||||
account_hash = get_account_hash(self.credential)
|
||||
|
||||
# Use title cache to get titles with fallback support
|
||||
return self.title_cache.get_cached_titles(
|
||||
titles = self.title_cache.get_cached_titles(
|
||||
title_id=str(title_id),
|
||||
fetch_function=self.get_titles,
|
||||
region=self.current_region,
|
||||
@@ -503,6 +503,17 @@ class Service(metaclass=ABCMeta):
|
||||
no_cache=no_cache,
|
||||
reset_cache=reset_cache,
|
||||
)
|
||||
return self.apply_title_map(titles)
|
||||
|
||||
def apply_title_map(self, titles: Titles_T) -> Titles_T:
|
||||
"""
|
||||
Rewrite service-provided titles using the per-service ``title_map`` config.
|
||||
|
||||
``title_map`` lives under ``services.<TAG>`` in unshackle.yaml. Applied after the
|
||||
title cache so config edits take effect without a cache reset, and before any
|
||||
``--enrich`` override so enrich wins. See ``remap_titles`` for the match rules.
|
||||
"""
|
||||
return remap_titles(titles, (self.config or {}).get("title_map") or {})
|
||||
|
||||
@abstractmethod
|
||||
def get_tracks(self, title: Title_T) -> Tracks:
|
||||
|
||||
@@ -8,4 +8,40 @@ Title_T = Union[Movie, Episode, Song]
|
||||
Titles_T = Union[Movies, Series, Album]
|
||||
|
||||
|
||||
__all__ = ("Episode", "Series", "Movie", "Movies", "Album", "Song", "Title_T", "Titles_T")
|
||||
def remap_titles(titles: Titles_T, title_map: dict) -> Titles_T:
|
||||
"""
|
||||
Rewrite titles in-place using an exact-match ``title_map``.
|
||||
|
||||
Some services name a title differently from how the user wants it stored, which can
|
||||
break library matching. ``title_map`` maps a source title string to the desired output
|
||||
title. Episodes are matched on their ``title`` (the show name), Movies and Songs on
|
||||
their ``name``. Returns the same collection for convenient chaining.
|
||||
"""
|
||||
if not title_map or not titles:
|
||||
return titles
|
||||
|
||||
def remap_one(title: Title_T) -> None:
|
||||
attr = "title" if isinstance(title, Episode) else "name"
|
||||
current = getattr(title, attr, None)
|
||||
if current and current in title_map:
|
||||
setattr(title, attr, title_map[current])
|
||||
|
||||
if hasattr(titles, "__iter__"):
|
||||
for title in titles:
|
||||
remap_one(title)
|
||||
else:
|
||||
remap_one(titles)
|
||||
return titles
|
||||
|
||||
|
||||
__all__ = (
|
||||
"Episode",
|
||||
"Series",
|
||||
"Movie",
|
||||
"Movies",
|
||||
"Album",
|
||||
"Song",
|
||||
"Title_T",
|
||||
"Titles_T",
|
||||
"remap_titles",
|
||||
)
|
||||
|
||||
@@ -655,6 +655,11 @@ services:
|
||||
muxing:
|
||||
set_title: true
|
||||
|
||||
# Remap service-provided titles before naming/output
|
||||
# Keyed by the exact title the service returns -> desired output title.
|
||||
title_map:
|
||||
Service Title: Desired Title
|
||||
|
||||
# Example: Service with different regions per profile
|
||||
SERVICE_NAME:
|
||||
profiles:
|
||||
|
||||
Reference in New Issue
Block a user