mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-12 20:15:10 +00:00
fix(services): match service aliases without case
Uppercase entries in ALIASES never resolved, because get_tag lowercased the input but not the alias. Match real tags first so a service's own tag cannot be taken by another service's alias.
This commit is contained in:
@@ -95,13 +95,13 @@ Declared at class level to configure framework behaviour:
|
||||
|
||||
| Variable | Type | Purpose |
|
||||
|---|---|---|
|
||||
| `ALIASES` | `tuple[str, ...]` | Extra tags that resolve to this service (e.g. `("EX", "DOMAIN")`). Default `()`. |
|
||||
| `GEOFENCE` | `tuple[str, ...]` | IP region codes the service requires. Empty = no geofence. The **first** entry is treated as the main region for auto-proxy. |
|
||||
| `ALIASES` | `tuple[str, ...]` | Extra tags that resolve to this service (e.g. `("EX", "DOMAIN")`). Case does not matter. Default `()`. |
|
||||
| `GEOFENCE` | `tuple[str, ...]` | IP region codes the service requires. Empty = no geofence. unshackle treats the **first** entry as the main region for auto-proxy. |
|
||||
| `VAULT_TAG` | `Optional[str]` | Overrides the key-vault namespace so sibling services can share one vault. Default `None` (use the service's own tag). |
|
||||
| `AUTH_METHODS` | `Optional[tuple[str, ...]]` | Auth methods accepted (`"cookies"` / `"credentials"`). When `None`, the REST `/services` endpoint infers them from `authenticate()`. |
|
||||
| `NO_SUBTITLES` | `bool` | Set `True` on a service with no subtitle tracks to skip subtitle handling entirely. |
|
||||
| `ANIME` | `bool` | Set `True` when the catalogue is anime, so metadata lookups prefer AniList. A title's own `anime` flag overrides it. |
|
||||
| `DAILY` | `bool` | Set `True` when the catalogue is daily/date-based (talk shows, news, sports), so episodes are named by air date. A title's own `daily` flag overrides it. |
|
||||
| `DAILY` | `bool` | Set `True` when the catalogue is daily/date-based (talk shows, news, sports), so unshackle names episodes by air date. A title's own `daily` flag overrides it. |
|
||||
|
||||
!!! note "`NO_SUBTITLES` is a convention, not a base-class attribute"
|
||||
`dl.py` uses `hasattr` to find `NO_SUBTITLES`, and the `Service` base class
|
||||
|
||||
@@ -61,12 +61,12 @@ Declared at the top of your service class, these are static descriptors:
|
||||
|
||||
| Class variable | Type | Purpose |
|
||||
|---|---|---|
|
||||
| `ALIASES` | `tuple[str, ...]` | Alternate tags the service answers to (resolved by `Services.get_tag`). |
|
||||
| `GEOFENCE` | `tuple[str, ...]` | ISO region codes the service requires; empty means no geofence. The first entry is treated as the "main region" for automatic proxy selection. |
|
||||
| `ALIASES` | `tuple[str, ...]` | Alternate tags the service answers to, in any case (resolved by `Services.get_tag`). |
|
||||
| `GEOFENCE` | `tuple[str, ...]` | ISO region codes the service requires; empty means no geofence. unshackle treats the first entry as the "main region" for automatic proxy selection. |
|
||||
| `VAULT_TAG` | `Optional[str]` | Overrides the key-vault namespace; defaults to the service's own tag. |
|
||||
| `AUTH_METHODS` | `Optional[tuple[str, ...]]` | Accepted auth methods (`"cookies"` / `"credentials"`). When `None`, the REST `/services` endpoint infers them from `authenticate()`. |
|
||||
| `ANIME` | `bool` | The catalogue is anime, so metadata lookups prefer AniList. A title's own `anime` flag overrides it. |
|
||||
| `DAILY` | `bool` | The catalogue is daily/date-based, so episodes are named by air date. A title's own `daily` flag overrides it. |
|
||||
| `DAILY` | `bool` | The catalogue is daily/date-based, so unshackle names episodes by air date. A title's own `daily` flag overrides it. |
|
||||
|
||||
### Required methods
|
||||
|
||||
@@ -141,7 +141,7 @@ families, one per media kind:
|
||||
All items subclass `Title`, which carries the shared identity: a **unique `id`**, the
|
||||
originating `service` class, an optional original `language`, arbitrary `data`, and a
|
||||
freshly-constructed `self.tracks = Tracks()` that `get_tracks` will fill. Collections
|
||||
are sorted lists, so titles stay in a stable order.
|
||||
use sorted lists, so titles stay in a stable order.
|
||||
|
||||
!!! warning "The unique ID is validated"
|
||||
`Title.__init__` rejects a falsy `id_`, and rejects any ID shorter than 4 characters
|
||||
@@ -330,7 +330,7 @@ in `unshackle.core.manifests`. Each parser has the same three-part contract:
|
||||
### The parser contract
|
||||
|
||||
1. **Construct**: `DASH.from_url(url, session=...)` or `DASH.from_text(text, url)`
|
||||
(and likewise for `HLS` / `ISM`). A `url` is always required, even for `from_text`,
|
||||
(and likewise for `HLS` / `ISM`). Always pass a `url`, even to `from_text`,
|
||||
because the parser needs it to make relative segment paths absolute.
|
||||
2. **`to_tracks(...)`**: parses the manifest into a `Tracks` container of `Video`,
|
||||
`Audio`, and `Subtitle` objects, each carrying descriptor-specific state.
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
"""Service tags resolve from an alias in any case, and a real tag beats another service's alias."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def two_services(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""SVCA claims "SVCB" as an alias, but SVCB is a service in its own right."""
|
||||
from unshackle.core import services
|
||||
|
||||
monkeypatch.setattr(services, "SERVICES", [Path("SVCA") / "__init__.py", Path("SVCB") / "__init__.py"])
|
||||
monkeypatch.setattr(services, "ALIASES", {"SVCA": ("SVCX", "SVCB"), "SVCB": ()})
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ["SVCX", "svcx", "SvCx"])
|
||||
def test_alias_resolves_in_any_case(two_services: None, value: str) -> None:
|
||||
from unshackle.core.services import Services
|
||||
|
||||
assert Services.get_tag(value) == "SVCA"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ["SVCB", "svcb", "SvCb"])
|
||||
def test_real_tag_beats_another_services_alias(two_services: None, value: str) -> None:
|
||||
from unshackle.core.services import Services
|
||||
|
||||
assert Services.get_tag(value) == "SVCB"
|
||||
|
||||
|
||||
def test_unknown_value_is_returned_unchanged(two_services: None) -> None:
|
||||
from unshackle.core.services import Services
|
||||
|
||||
assert Services.get_tag("NOPE") == "NOPE"
|
||||
@@ -171,7 +171,7 @@ class Service(metaclass=ABCMeta):
|
||||
still, during track download.
|
||||
"""
|
||||
|
||||
ALIASES: tuple[str, ...] = () # list of aliases for the service; alternatives to the service tag.
|
||||
ALIASES: tuple[str, ...] = () # alternative tags for the service, matched without case.
|
||||
GEOFENCE: tuple[str, ...] = () # list of ip regions required to use the service. empty list == no specific region.
|
||||
ANIME: bool = False # service catalogue is anime; metadata lookups prefer AniList. Title.anime overrides per title.
|
||||
DAILY: bool = False # catalog is daily/date-based. episodes are named by air date. Title.daily overrides per title.
|
||||
@@ -489,8 +489,8 @@ class Service(metaclass=ABCMeta):
|
||||
read by the Cdm and decrypted.
|
||||
|
||||
This is a very important request to get correct. A bad, unexpected, or missing
|
||||
value in the request can cause the service to detect your CDM device. The device
|
||||
can then be banned, revoked, disabled, or downgraded.
|
||||
value in the request can cause the service to detect your CDM device.
|
||||
The service can then ban, revoke, disable, or downgrade that device.
|
||||
|
||||
:param challenge: The license challenge from the Widevine CDM.
|
||||
:param title: The current `Title` from get_titles that unshackle processes now. unshackle
|
||||
@@ -510,8 +510,8 @@ class Service(metaclass=ABCMeta):
|
||||
read by the CDM and decrypted.
|
||||
|
||||
This is a very important request to get correct. A bad, unexpected, or missing
|
||||
value in the request can cause the service to detect your CDM device. The device
|
||||
can then be banned, revoked, disabled, or downgraded.
|
||||
value in the request can cause the service to detect your CDM device.
|
||||
The service can then ban, revoke, disable, or downgrade that device.
|
||||
|
||||
:param challenge: The license challenge from the PlayReady CDM.
|
||||
:param title: The current `Title` from get_titles that unshackle processes now. unshackle
|
||||
|
||||
@@ -317,18 +317,22 @@ class Services(click.Group):
|
||||
def get_tag(value: str) -> str:
|
||||
"""
|
||||
Get the Service Tag (the name of the service's directory, not one of its aliases) by an Alias.
|
||||
Input value can be of any case-sensitivity.
|
||||
Input value can be of any case-sensitivity, and so can the alias it matches.
|
||||
A real tag always wins over an alias: services do declare aliases that collide with another service's tag.
|
||||
This method returns the original input value if it does not match a service tag.
|
||||
"""
|
||||
original_value = value
|
||||
value = value.lower()
|
||||
value_lower = value.lower()
|
||||
tags = [path.parent.stem for path in SERVICES]
|
||||
|
||||
for path in SERVICES:
|
||||
tag = path.parent.stem
|
||||
if value in (tag.lower(), *ALIASES.get(tag, [])):
|
||||
for tag in tags:
|
||||
if value_lower == tag.lower():
|
||||
return tag
|
||||
|
||||
return original_value
|
||||
for tag in tags:
|
||||
if any(value_lower == alias.lower() for alias in ALIASES.get(tag, ())):
|
||||
return tag
|
||||
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def load(tag: str) -> Service:
|
||||
|
||||
Reference in New Issue
Block a user