Fixed MUBI with UHD and Added Search

This commit is contained in:
FairTrade 2026-02-06 17:02:24 +01:00
parent 2c09bce845
commit 3e0732987c
3 changed files with 184 additions and 125 deletions

View File

@ -7,61 +7,63 @@ from langcodes import Language
import base64 import base64
import click import click
from unshackle.core.constants import AnyTrack from unshackle.core.constants import AnyTrack
from unshackle.core.credential import Credential
from unshackle.core.manifests import DASH from unshackle.core.manifests import DASH
from unshackle.core.service import Service from unshackle.core.service import Service
from unshackle.core.credential import Credential
from unshackle.core.titles import Episode, Movie, Movies, Title_T, Titles_T, Series from unshackle.core.titles import Episode, Movie, Movies, Title_T, Titles_T, Series
from unshackle.core.tracks import Chapter, Tracks, Subtitle from unshackle.core.tracks import Chapter, Tracks, Subtitle
from unshackle.core.search_result import SearchResult
class MUBI(Service): class MUBI(Service):
""" """
Service code for MUBI (mubi.com) Service code for MUBI (mubi.com)
Version: 1.2.0 Version: 1.2.1 (Cookie-only + Auto-UHD + Search)
Authorization: Cookies ONLY (lt token + _mubi_session)
Authorization: Required cookies (lt token + session) Security: UHD @ L3/SL2K (Widevine/PlayReady)
Security: FHD @ L3 (Widevine)
Supports: Supports:
Series https://mubi.com/en/nl/series/twin-peaks Series https://mubi.com/en/nl/series/twin-peaks
Movies https://mubi.com/en/nl/films/the-substance Movies https://mubi.com/en/nl/films/the-substance
""" """
SERIES_TITLE_RE = r"^https?://(?:www\.)?mubi\.com(?:/[^/]+)*?/series/(?P<series_slug>[^/]+)(?:/season/(?P<season_slug>[^/]+))?$" SERIES_TITLE_RE = r"^https?://(?:www\.)?mubi\.com(?:/[^/]+)*?/series/(?P<series_slug>[^/]+)(?:/season/(?P<season_slug>[^/]+))?$"
TITLE_RE = r"^(?:https?://(?:www\.)?mubi\.com)(?:/[^/]+)*?/films/(?P<slug>[^/?#]+)$" TITLE_RE = r"^(?:https?://(?:www\.)?mubi\.com)(?:/[^/]+)*?/films/(?P<slug>[^/?#]+)$"
NO_SUBTITLES = False NO_SUBTITLES = False
@staticmethod @staticmethod
@click.command(name="MUBI", short_help="https://mubi.com") @click.command(name="MUBI", short_help="https://mubi.com ")
@click.argument("title", type=str) @click.argument("title", type=str)
@click.option("-c", "--country", default=None, type=str,
help="With VPN set country code other than the one assigned to the account.")
@click.pass_context @click.pass_context
def cli(ctx, **kwargs): def cli(ctx, **kwargs):
return MUBI(ctx, **kwargs) return MUBI(ctx, **kwargs)
def __init__(self, ctx, title: str): def __init__(self, ctx, title: str, country: str):
super().__init__(ctx) super().__init__(ctx)
self.raw_title = title # Store raw input for search mode
self.country = country
# Only parse as URL if it matches MUBI patterns
m_film = re.match(self.TITLE_RE, title) m_film = re.match(self.TITLE_RE, title)
m_series = re.match(self.SERIES_TITLE_RE, title) m_series = re.match(self.SERIES_TITLE_RE, title)
if not m_film and not m_series:
raise ValueError(f"Invalid MUBI URL: {title}")
self.is_series = bool(m_series) self.is_series = bool(m_series)
self.slug = m_film.group("slug") if m_film else None self.slug = m_film.group("slug") if m_film else None
self.series_slug = m_series.group("series_slug") if m_series else None self.series_slug = m_series.group("series_slug") if m_series else None
self.season_slug = m_series.group("season_slug") if m_series else None self.season_slug = m_series.group("season_slug") if m_series else None
# Core state
self.film_id: Optional[int] = None self.film_id: Optional[int] = None
self.lt_token: Optional[str] = None self.lt_token: Optional[str] = None
self.session_token: Optional[str] = None self.session_token: Optional[str] = None
self.user_id: Optional[int] = None self.user_id: Optional[int] = None
self.country_code: Optional[str] = None self.country_code: Optional[str] = None
self.set_country_code: Optional[str] = country
self.anonymous_user_id: Optional[str] = None self.anonymous_user_id: Optional[str] = None
self.default_country: Optional[str] = None self.default_country: Optional[str] = None
self.reels_data: Optional[list] = None self.reels_data: Optional[list] = None
# Store CDM reference # ALWAYS enable UHD/HEVC path - no user flag required
self.uhd = True
self.cdm = ctx.obj.cdm self.cdm = ctx.obj.cdm
if self.config is None: if self.config is None:
@ -70,6 +72,10 @@ class MUBI(Service):
def authenticate(self, cookies: Optional[CookieJar] = None, credential: Optional[Credential] = None) -> None: def authenticate(self, cookies: Optional[CookieJar] = None, credential: Optional[Credential] = None) -> None:
super().authenticate(cookies, credential) super().authenticate(cookies, credential)
if not cookies:
raise PermissionError("MUBI requires login cookies (lt + _mubi_session). Credentials login is not supported.")
# IP geolocation for country detection
try: try:
r_ip = self.session.get(self.config["endpoints"]["ip_geolocation"], timeout=5) r_ip = self.session.get(self.config["endpoints"]["ip_geolocation"], timeout=5)
r_ip.raise_for_status() r_ip.raise_for_status()
@ -82,10 +88,7 @@ class MUBI(Service):
except Exception as e: except Exception as e:
raise ValueError(f"Failed to fetch IP geolocation: {e}") raise ValueError(f"Failed to fetch IP geolocation: {e}")
if not cookies: # Extract essential tokens from cookies
raise PermissionError("MUBI requires login cookies.")
# Extract essential tokens
lt_cookie = next((c for c in cookies if c.name == "lt"), None) lt_cookie = next((c for c in cookies if c.name == "lt"), None)
session_cookie = next((c for c in cookies if c.name == "_mubi_session"), None) session_cookie = next((c for c in cookies if c.name == "_mubi_session"), None)
snow_id_cookie = next((c for c in cookies if c.name == "_snow_id.c006"), None) snow_id_cookie = next((c for c in cookies if c.name == "_snow_id.c006"), None)
@ -98,20 +101,21 @@ class MUBI(Service):
self.lt_token = lt_cookie.value self.lt_token = lt_cookie.value
self.session_token = session_cookie.value self.session_token = session_cookie.value
# Extract anonymous_user_id from _snow_id.c006 # Extract or generate anonymous_user_id
if snow_id_cookie and "." in snow_id_cookie.value: if snow_id_cookie and "." in snow_id_cookie.value:
self.anonymous_user_id = snow_id_cookie.value.split(".")[0] self.anonymous_user_id = snow_id_cookie.value.split(".")[0]
else: else:
self.anonymous_user_id = str(uuid.uuid4()) self.anonymous_user_id = str(uuid.uuid4())
self.log.warning(f"No _snow_id.c006 cookie found — generated new anonymous_user_id: {self.anonymous_user_id}") self.log.warning(f"No _snow_id.c006 cookie found — generated new anonymous_user_id: {self.anonymous_user_id}")
# Configure session headers for UHD access
base_headers = { base_headers = {
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) Firefox/143.0", "User-Agent": "Mozilla/5.0 (Linux; Android 13; SM-G975F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36",
"Origin": "https://mubi.com", "Origin": "https://mubi.com",
"Referer": "https://mubi.com/", "Referer": "https://mubi.com/",
"CLIENT": "web", "CLIENT": "web",
"Client-Accept-Video-Codecs": "h265,vp9,h264", "Client-Accept-Video-Codecs": "h265,vp9,h264",
"Client-Accept-Audio-Codecs": "aac", "Client-Accept-Audio-Codecs": "eac3,ac3,aac",
"Authorization": f"Bearer {self.lt_token}", "Authorization": f"Bearer {self.lt_token}",
"ANONYMOUS_USER_ID": self.anonymous_user_id, "ANONYMOUS_USER_ID": self.anonymous_user_id,
"Client-Country": self.default_country, "Client-Country": self.default_country,
@ -121,9 +125,9 @@ class MUBI(Service):
"Pragma": "no-cache", "Pragma": "no-cache",
"Cache-Control": "no-cache", "Cache-Control": "no-cache",
} }
self.session.headers.update(base_headers) self.session.headers.update(base_headers)
# Fetch account info
r_account = self.session.get(self.config["endpoints"]["account"]) r_account = self.session.get(self.config["endpoints"]["account"])
if not r_account.ok: if not r_account.ok:
raise PermissionError(f"Failed to fetch MUBI account: {r_account.status_code} {r_account.text}") raise PermissionError(f"Failed to fetch MUBI account: {r_account.status_code} {r_account.text}")
@ -132,9 +136,11 @@ class MUBI(Service):
self.user_id = account_data.get("id") self.user_id = account_data.get("id")
self.country_code = (account_data.get("country") or {}).get("code", "NL") self.country_code = (account_data.get("country") or {}).get("code", "NL")
if self.set_country_code is not None:
self.country_code = self.set_country_code.upper()
self.session.headers["Client-Country"] = self.country_code self.session.headers["Client-Country"] = self.country_code
self.GEOFENCE = (self.country_code,) self.GEOFENCE = (self.country_code,)
self._bind_anonymous_user() self._bind_anonymous_user()
self.log.info( self.log.info(
@ -168,24 +174,22 @@ class MUBI(Service):
r = self.session.get(url) r = self.session.get(url)
r.raise_for_status() r.raise_for_status()
data = r.json() data = r.json()
self.film_id = data["id"] self.film_id = data["id"]
# Fetch reels to get definitive language code and cache the response # Fetch reels for language detection and subtitle names
url_reels = self.config["endpoints"]["reels"].format(film_id=self.film_id) url_reels = self.config["endpoints"]["reels"].format(film_id=self.film_id)
r_reels = self.session.get(url_reels) r_reels = self.session.get(url_reels)
r_reels.raise_for_status() r_reels.raise_for_status()
self.reels_data = r_reels.json() self.reels_data = r_reels.json()
# Extract original language from the first audio track of the first reel # Detect original language from first audio track
original_language_code = "en" # Default fallback original_language_code = "en"
if self.reels_data and self.reels_data[0].get("audio_tracks"): if self.reels_data and self.reels_data[0].get("audio_tracks"):
first_audio_track = self.reels_data[0]["audio_tracks"][0] first_audio_track = self.reels_data[0]["audio_tracks"][0]
if "language_code" in first_audio_track: if "language_code" in first_audio_track:
original_language_code = first_audio_track["language_code"] original_language_code = first_audio_track["language_code"]
self.log.debug(f"Detected original language from reels: '{original_language_code}'") self.log.debug(f"Detected original language from reels: '{original_language_code}'")
genres = ", ".join(data.get("genres", [])) or "Unknown"
description = ( description = (
data.get("default_editorial_html", "") data.get("default_editorial_html", "")
.replace("<p>", "").replace("</p>", "").replace("<em>", "").replace("</em>", "").strip() .replace("<p>", "").replace("</p>", "").replace("<em>", "").replace("</em>", "").strip()
@ -202,19 +206,15 @@ class MUBI(Service):
language=Language.get(original_language_code), language=Language.get(original_language_code),
data=data, data=data,
) )
return Movies([movie]) return Movies([movie])
def _get_series_titles(self) -> Titles_T: def _get_series_titles(self) -> Titles_T:
# Fetch series metadata
series_url = self.config["endpoints"]["series"].format(series_slug=self.series_slug) series_url = self.config["endpoints"]["series"].format(series_slug=self.series_slug)
r_series = self.session.get(series_url) r_series = self.session.get(series_url)
r_series.raise_for_status() r_series.raise_for_status()
series_data = r_series.json() series_data = r_series.json()
episodes = [] episodes = []
# If season is explicitly specified, only fetch that season
if self.season_slug: if self.season_slug:
eps_url = self.config["endpoints"]["season_episodes"].format( eps_url = self.config["endpoints"]["season_episodes"].format(
series_slug=self.series_slug, series_slug=self.series_slug,
@ -227,62 +227,48 @@ class MUBI(Service):
episodes_data = r_eps.json().get("episodes", []) episodes_data = r_eps.json().get("episodes", [])
self._add_episodes_to_list(episodes, episodes_data, series_data) self._add_episodes_to_list(episodes, episodes_data, series_data)
else: else:
# No season specified fetch ALL seasons
seasons = series_data.get("seasons", []) seasons = series_data.get("seasons", [])
if not seasons: if not seasons:
raise ValueError("No seasons found for this series.") raise ValueError("No seasons found for this series.")
for season in seasons: for season in seasons:
season_slug = season["slug"] season_slug = season["slug"]
eps_url = self.config["endpoints"]["season_episodes"].format( eps_url = self.config["endpoints"]["season_episodes"].format(
series_slug=self.series_slug, series_slug=self.series_slug,
season_slug=season_slug season_slug=season_slug
) )
self.log.debug(f"Fetching episodes for season: {season_slug}") self.log.debug(f"Fetching episodes for season: {season_slug}")
r_eps = self.session.get(eps_url) r_eps = self.session.get(eps_url)
# Stop if season returns 404 or empty
if r_eps.status_code == 404: if r_eps.status_code == 404:
self.log.info(f"Season '{season_slug}' not available, skipping.") self.log.info(f"Season '{season_slug}' not available, skipping.")
continue continue
r_eps.raise_for_status() r_eps.raise_for_status()
episodes_data = r_eps.json().get("episodes", []) episodes_data = r_eps.json().get("episodes", [])
if not episodes_data: if not episodes_data:
self.log.info(f"No episodes found in season '{season_slug}'.") self.log.info(f"No episodes found in season '{season_slug}'.")
continue continue
self._add_episodes_to_list(episodes, episodes_data, series_data) self._add_episodes_to_list(episodes, episodes_data, series_data)
from unshackle.core.titles import Series
return Series(sorted(episodes, key=lambda x: (x.season, x.number))) return Series(sorted(episodes, key=lambda x: (x.season, x.number)))
def _add_episodes_to_list(self, episodes_list: list, episodes_data: list, series_data: dict): def _add_episodes_to_list(self, episodes_list: list, episodes_data: list, series_data: dict):
"""Helper to avoid code duplication when adding episodes."""
for ep in episodes_data: for ep in episodes_data:
# Use episode's own language detection via its consumable.playback_languages
playback_langs = ep.get("consumable", {}).get("playback_languages", {}) playback_langs = ep.get("consumable", {}).get("playback_languages", {})
audio_langs = playback_langs.get("audio_options", ["English"]) audio_langs = playback_langs.get("audio_options", ["English"])
lang_code = audio_langs[0].split()[0].lower() if audio_langs else "en" lang_code = audio_langs[0].split()[0].lower() if audio_langs else "en"
try: try:
detected_lang = Language.get(lang_code) detected_lang = Language.get(lang_code)
except: except:
detected_lang = Language.get("en") detected_lang = Language.get("en")
episodes_list.append(Episode( episodes_list.append(Episode(
id_=ep["id"], id_=ep["id"],
service=self.__class__, service=self.__class__,
title=series_data["title"], # Series title title=series_data["title"],
season=ep["episode"]["season_number"], season=ep["episode"]["season_number"],
number=ep["episode"]["number"], number=ep["episode"]["number"],
name=ep["title"], # Episode title name=ep["title"],
description=ep.get("short_synopsis", ""), description=ep.get("short_synopsis", ""),
language=detected_lang, language=detected_lang,
data=ep, # Full episode data for later use in get_tracks data=ep,
)) ))
def get_tracks(self, title: Title_T) -> Tracks: def get_tracks(self, title: Title_T) -> Tracks:
@ -290,60 +276,77 @@ class MUBI(Service):
if not film_id: if not film_id:
raise RuntimeError("Title ID not found.") raise RuntimeError("Title ID not found.")
# For series episodes, we don't have reels cached, so skip reel-based logic # Initiate viewing session
url_view = self.config["endpoints"]["initiate_viewing"].format(film_id=film_id) url_view = self.config["endpoints"]["initiate_viewing"].format(film_id=film_id)
r_view = self.session.post(url_view, json={}, headers={"Content-Type": "application/json"}) r_view = self.session.post(url_view, json={}, headers={"Content-Type": "application/json"})
r_view.raise_for_status() r_view.raise_for_status()
view_data = r_view.json() view_data = r_view.json()
reel_id = view_data["reel_id"] reel_id = view_data["reel_id"]
# For films, use reels data for language/audio mapping # Fetch reels data if not cached
if not self.is_series:
if not self.film_id: if not self.film_id:
raise RuntimeError("film_id not set. Call get_titles() first.") self.film_id = film_id
if not self.reels_data: if not self.reels_data:
self.log.warning("Reels data not cached, fetching now.")
url_reels = self.config["endpoints"]["reels"].format(film_id=film_id) url_reels = self.config["endpoints"]["reels"].format(film_id=film_id)
r_reels = self.session.get(url_reels) r_reels = self.session.get(url_reels)
r_reels.raise_for_status() r_reels.raise_for_status()
reels = r_reels.json() self.reels_data = r_reels.json()
else:
reels = self.reels_data reels = self.reels_data
text_tracks_reel = reels[0]["text_tracks"]
reel = next((r for r in reels if r["id"] == reel_id), reels[0]) reel = next((r for r in reels if r["id"] == reel_id), reels[0])
else:
# For episodes, we dont need reel-based logic — just proceed
pass
# Request secure streaming URL, works for both films and episodes # Get secure streaming URL
url_secure = self.config["endpoints"]["secure_url"].format(film_id=film_id) url_secure = self.config["endpoints"]["secure_url"].format(film_id=film_id)
r_secure = self.session.get(url_secure) r_secure = self.session.get(url_secure)
r_secure.raise_for_status() r_secure.raise_for_status()
secure_data = r_secure.json() secure_data = r_secure.json()
# Find DASH manifest URL
manifest_url = None manifest_url = None
for entry in secure_data.get("urls", []): for entry in secure_data.get("urls", []):
if entry.get("content_type") == "application/dash+xml": if entry.get("content_type") == "application/dash+xml":
manifest_url = entry["src"] manifest_url = entry["src"]
break break
if not manifest_url: if not manifest_url:
raise ValueError("No DASH manifest URL found.") raise ValueError("No DASH manifest URL found.")
# Parse DASH, use title.language as fallback manifest_url = re.sub(
r'/default/ver1\.AVC1\.[^/]*\.mpd',
'/default/ver1.hevc.ex-vtt.mpd',
manifest_url
)
# Fallback for non-AVC URLs
if '/default/ver1.hevc.ex-vtt.mpd' not in manifest_url:
manifest_url = re.sub(
r'/default/[^/]*\.mpd',
'/default/ver1.hevc.ex-vtt.mpd',
manifest_url
)
# Parse DASH manifest
tracks = DASH.from_url(manifest_url, session=self.session).to_tracks(language=title.language) tracks = DASH.from_url(manifest_url, session=self.session).to_tracks(language=title.language)
# Add subtitles # Add enhanced subtitles (forced/SDH detection)
subtitles = [] subtitles = []
for sub in secure_data.get("text_track_urls", []): for sub in secure_data.get("text_track_urls", []):
lang_code = sub.get("language_code", "und") lang_code = sub.get("language_code", "und")
vtt_url = sub.get("url") vtt_url = sub.get("url")
role = sub.get("role")
forced = False
sdh = False
if not vtt_url: if not vtt_url:
continue continue
try:
disp_name = (next(filter(lambda x: x['id'] == sub["id"], text_tracks_reel), None))["display_name"]
except:
disp_name = sub.get("role", "") + " " + lang_code.upper()
if role == "forced-subtitle":
forced = True
if role == "caption":
sdh = True
if "(SDH)" in disp_name:
disp_name = disp_name.replace("(SDH)", "").strip()
is_original = lang_code == title.language.language is_original = lang_code == title.language.language
subtitles.append( subtitles.append(
Subtitle( Subtitle(
id_=sub["id"], id_=sub["id"],
@ -351,15 +354,43 @@ class MUBI(Service):
language=Language.get(lang_code), language=Language.get(lang_code),
is_original_lang=is_original, is_original_lang=is_original,
codec=Subtitle.Codec.WebVTT, codec=Subtitle.Codec.WebVTT,
name=sub.get("display_name", lang_code.upper()), name=disp_name,
forced=False, forced=forced,
sdh=False, sdh=sdh,
) )
) )
tracks.subtitles = subtitles tracks.subtitles = subtitles
return tracks return tracks
def search(self) -> Generator[SearchResult, None, None]:
"""
Search MUBI films using official API endpoint.
Returns only playable films with proper metadata formatting.
"""
params = {
"query": self.raw_title,
"page": 1,
"per_page": 24,
"playable": "true",
"all_films_on_zero_hits": "true"
}
response = self.session.get(
url=self.config["endpoints"]["search"],
params=params
)
response.raise_for_status()
results = response.json()
for film in results.get("films", []):
display_title = f"{film['title']} ({film['year']})"
yield SearchResult(
id_=film["id"],
title=display_title,
label="MOVIE",
url=film["web_url"].rstrip() # Clean trailing spaces
)
def get_chapters(self, title: Title_T) -> list[Chapter]: def get_chapters(self, title: Title_T) -> list[Chapter]:
return [] return []
@ -367,22 +398,20 @@ class MUBI(Service):
if not self.user_id: if not self.user_id:
raise RuntimeError("user_id not set — authenticate first.") raise RuntimeError("user_id not set — authenticate first.")
# Cookie-based license request (NO dtinfo - credentials removed)
dt_custom_data = { dt_custom_data = {
"userId": self.user_id, "userId": self.user_id,
"sessionId": self.lt_token, "sessionId": self.lt_token,
"merchant": "mubi" "merchant": "mubi"
} }
dt_custom_data_b64 = base64.b64encode(json.dumps(dt_custom_data).encode()).decode() dt_custom_data_b64 = base64.b64encode(json.dumps(dt_custom_data).encode()).decode()
headers = { headers = {
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:143.0) Gecko/20100101 Firefox/143.0", "User-Agent": "Mozilla/5.0 (Linux; Android 13; SM-G975F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36",
"Accept": "*/*", "Accept": "*/*",
"Origin": "https://mubi.com", "Origin": "https://mubi.com",
"Referer": "https://mubi.com/", "Referer": "https://mubi.com/",
"dt-custom-data": dt_custom_data_b64, "dt-custom-data": dt_custom_data_b64,
} }
r = self.session.post( r = self.session.post(
self.config["endpoints"]["license"], self.config["endpoints"]["license"],
data=challenge, data=challenge,
@ -394,3 +423,30 @@ class MUBI(Service):
raise PermissionError(f"DRM license error: {license_data}") raise PermissionError(f"DRM license error: {license_data}")
return base64.b64decode(license_data["license"]) return base64.b64decode(license_data["license"])
def get_playready_license(self, challenge: bytes, title: Title_T, track: AnyTrack) -> bytes:
if not self.user_id:
raise RuntimeError("user_id not set — authenticate first.")
# Cookie-based PlayReady license request (NO dtinfo - credentials removed)
dt_custom_data = {
"userId": self.user_id,
"sessionId": self.lt_token,
"merchant": "mubi"
}
dt_custom_data_b64 = base64.b64encode(json.dumps(dt_custom_data).encode()).decode()
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36 Edg/144.0.0.0",
"Accept": "*/*",
"Origin": "https://mubi.com",
"Referer": "https://mubi.com/",
"dt-custom-data": dt_custom_data_b64,
}
r = self.session.post(
self.config["endpoints"]["license_pr"],
data=challenge,
headers=headers,
)
r.raise_for_status()
if r.status_code != 200:
raise PermissionError(f"DRM license error")
return r.content

View File

@ -10,3 +10,5 @@ endpoints:
ip_geolocation: "https://directory.cookieyes.com/api/v1/ip" ip_geolocation: "https://directory.cookieyes.com/api/v1/ip"
series: "https://api.mubi.com/v4/series/{series_slug}" series: "https://api.mubi.com/v4/series/{series_slug}"
season_episodes: "https://api.mubi.com/v4/series/{series_slug}/seasons/{season_slug}/episodes/available" season_episodes: "https://api.mubi.com/v4/series/{series_slug}/seasons/{season_slug}/episodes/available"
license_pr: "https://lic.drmtoday.com/license-proxy-headerauth/drmtoday/RightsManager.asmx?persistent=false"
search: "https://api.mubi.com/v4/search/films"

View File

@ -19,7 +19,7 @@
4. HIDI: 4. HIDI:
- Subtitle is a bit misplace if second sentences came up making the last sentence on the first order and vice versa (needs to be fixed) - Subtitle is a bit misplace if second sentences came up making the last sentence on the first order and vice versa (needs to be fixed)
5. MUBI: 5. MUBI:
- Search Functionality - Creds login
6. VIKI: 6. VIKI:
- CSRF Token is now scraped, would be from a api requests soon - CSRF Token is now scraped, would be from a api requests soon
7. VIDO: 7. VIDO:
@ -41,4 +41,5 @@
- Acknowledgment - Acknowledgment
Thanks to Adef for the NPO start downloader. Thanks to Adef for the NPO start downloader.
Thanks to UPS0 for fixing MUBI script