From 5e1d2aa94a3110edc9613b5d4f2efce47a1a8dc4 Mon Sep 17 00:00:00 2001 From: Mike Date: Sun, 25 Jan 2026 15:42:34 +0200 Subject: [PATCH] Added Tabii Services thanks to Riyadh --- Tabii/__init__.py | 599 +++++++++++++++++++++ Tabii/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 32464 bytes Tabii/config.yaml | 37 ++ 3 files changed, 636 insertions(+) create mode 100644 Tabii/__init__.py create mode 100644 Tabii/__pycache__/__init__.cpython-312.pyc create mode 100644 Tabii/config.yaml diff --git a/Tabii/__init__.py b/Tabii/__init__.py new file mode 100644 index 0000000..8d93150 --- /dev/null +++ b/Tabii/__init__.py @@ -0,0 +1,599 @@ +import hashlib +import json +import re +from collections.abc import Generator +from datetime import datetime +from http.cookiejar import CookieJar +from typing import Optional, Union + +import click +from langcodes import Language + +from unshackle.core.constants import AnyTrack +from unshackle.core.credential import Credential +from unshackle.core.manifests import DASH +from unshackle.core.search_result import SearchResult +from unshackle.core.service import Service +from unshackle.core.titles import Episode, Movie, Movies, Series, Title_T, Titles_T +from unshackle.core.tracks import Chapter, Subtitle, Tracks, Video + + +class Tabii(Service): + """ + Service code for Tabii (tabii.com) + Version: 1.1.0 + Author: Riyadh + + \b + Authorization: Credentials + Security: FHD@L3 + + \b + Supported URL formats: + - Direct title ID: 527983 + - Watch URL: https://www.tabii.com/watch/527983 + - Browse URL: https://www.tabii.com/browse/149106_149112?dt=527983 + - Episode URL: https://www.tabii.com/watch/527983?trackId=545891 + - Movie URL: https://www.tabii.com/browse/149265_149266?dt=544916 + + \b + Features: + - Automatic token caching and refresh + - Support for both movies and series + - Multi-language audio and subtitles + - Widevine L3 DRM decryption + """ + + TITLE_RE = r"^(?:https?://(?:www\.)?tabii\.com/(?:watch|browse)/(?:[^?]+\?dt=)?)?(?P[^/?&]+)(?:\?trackId=(?P[^&]+))?" + GEOFENCE = ("TR",) + + @staticmethod + @click.command(name="Tabii", short_help="https://tabii.com") + @click.argument("title", type=str) + @click.option("-m", "--movie", is_flag=True, default=False, help="Specify if it's a movie") + @click.pass_context + def cli(ctx, **kwargs): + return Tabii(ctx, **kwargs) + + def __init__(self, ctx, title, movie): + super().__init__(ctx) + + self.title = title + self.movie = movie + self.cdm = ctx.obj.cdm + + if self.config is None: + raise Exception("Tabii service configuration is missing! Please add config.yaml to the service directory.") + + self.access_token = None + self.refresh_token = None + self.device = "android" + self.license_url = None + + # Validate required config sections + required_sections = ["endpoints", "client"] + for section in required_sections: + if section not in self.config: + raise Exception(f"Missing required config section: {section}") + + def authenticate(self, cookies: Optional[CookieJar] = None, credential: Optional[Credential] = None) -> None: + """Authenticate with Tabii service using email and password""" + super().authenticate(cookies, credential) + + if not credential: + raise EnvironmentError("Tabii requires email and password credentials for authentication.") + + # Setup basic session headers + self.log.debug("Setting up session headers") + self.session.headers.update({ + "User-Agent": self.config["client"][self.device]["user_agent"], + "Accept-Encoding": "gzip", + "Accept-Language": "en", + "Connection": "Keep-Alive" + }) + + # Try cached tokens first + cache = self.cache.get(f"tabii_tokens") + + if cache and cache.data: + expires_at = cache.data.get("expires_at", 0) + current_time = int(datetime.now().timestamp()) + + if expires_at > current_time: + self.log.info("Using cached authentication tokens") + self.log.debug(f"Token valid for {(expires_at - current_time) // 60} more minutes") + self.access_token = cache.data["access_token"] + self.refresh_token = cache.data["refresh_token"] + self._set_full_headers() + else: + self.log.info("Cached tokens expired, attempting refresh") + if not self._refresh_token(cache.data.get("refresh_token")): + self.log.warning("Token refresh failed, performing new login") + self._perform_login(credential.username, credential.password) + else: + self.log.info("Successfully refreshed authentication tokens") + else: + self.log.info("No cached tokens found, performing new login") + self._perform_login(credential.username, credential.password) + + # Set authorization header with final token + self.session.headers.update({"Authorization": f"Bearer {self.access_token}"}) + self.log.debug("Authentication completed successfully") + + def _set_full_headers(self) -> None: + """Set all required headers after authentication""" + self.log.debug("Configuring full session headers") + self.session.headers.update(self.config["client"][self.device]["headers"]) + self.session.headers.update({"User-Agent": self.config["client"][self.device]["user_agent"]}) + + def _perform_login(self, email: str, password: str) -> None: + """Perform complete login flow: initial auth -> profile selection -> profile token""" + + self.log.info("Starting Tabii authentication flow") + + # Step 1: Initial login + self.log.debug("Step 1/3: Requesting initial access token") + login_headers = { + "Content-Type": "application/json", + "Accept-Encoding": "gzip", + "Connection": "Keep-Alive", + "Trtdigitalprod": self.config["client"][self.device]["headers"]["Trtdigitalprod"], + "User-Agent": self.config["client"][self.device]["user_agent"], + "x-client-useragent": self.config["client"][self.device]["headers"]["x-client-useragent"], + "x-clientid": self.config["client"][self.device]["headers"]["x-clientid"], + "x-clientpass": self.config["client"][self.device]["headers"]["x-clientpass"], + "x-uid": self.config["client"][self.device]["headers"]["x-uid"] + } + + try: + login_req = self.session.post( + url=self.config["endpoints"]["login"], + json={"email": email, "password": password}, + headers=login_headers, + timeout=30 + ) + + if login_req.status_code != 200: + error_msg = f"Login failed with HTTP {login_req.status_code}" + try: + error_data = login_req.json() + if "message" in error_data: + error_msg += f": {error_data['message']}" + except: + error_msg += f": {login_req.text[:200]}" + + self.log.error(error_msg) + raise Exception(error_msg) + + login_response = login_req.json() + self.log.debug("Initial authentication successful") + + except Exception as e: + self.log.error(f"Login request failed: {str(e)}") + raise Exception(f"Failed to authenticate with Tabii: {str(e)}") + + profile_access_token = login_response.get("accessToken") + profile_refresh_token = login_response.get("refreshToken") + + if not profile_access_token or not profile_refresh_token: + error_msg = login_response.get("message", "Unknown error - missing tokens in response") + self.log.error(f"Failed to obtain access tokens: {error_msg}") + raise Exception(f"Authentication failed: {error_msg}") + + # Step 2: Get profiles + self.log.debug("Step 2/3: Fetching available profiles") + self.session.headers.update({"Authorization": f"Bearer {profile_access_token}"}) + + try: + profiles_response = self.session.get( + url=self.config["endpoints"]["profiles"], + timeout=30 + ).json() + except Exception as e: + self.log.error(f"Failed to fetch profiles: {str(e)}") + raise Exception(f"Could not retrieve profile list: {str(e)}") + + # Find non-kids profile + profile_sk = None + profile_name = None + profiles_data = profiles_response.get("data", []) + + self.log.debug(f"Found {len(profiles_data)} profile(s)") + + for profile in profiles_data: + kids_value = profile.get("kids", False) + if isinstance(kids_value, str): + kids_value = kids_value.lower() == "true" + + if not kids_value: + profile_sk = profile.get("SK") + profile_name = profile.get("name", "Unknown") + self.log.debug(f"Selected profile: {profile_name}") + break + + if not profile_sk: + self.log.error("No suitable adult profile found in account") + raise Exception("No adult profile available. Please create a non-kids profile in your Tabii account.") + + # Step 3: Get profile token + self.log.debug(f"Step 3/3: Requesting token for profile '{profile_name}'") + + try: + profile_token_response = self.session.post( + url=self.config["endpoints"]["profile_token"].format(profile_sk=profile_sk), + json={"refreshToken": profile_refresh_token}, + headers={"Content-Type": "application/json"}, + timeout=30 + ).json() + except Exception as e: + self.log.error(f"Failed to get profile token: {str(e)}") + raise Exception(f"Could not obtain profile-specific token: {str(e)}") + + self.access_token = profile_token_response.get("accessToken") + self.refresh_token = profile_token_response.get("refreshToken") + + if not self.access_token or not self.refresh_token: + self.log.error("Profile token response missing required tokens") + raise Exception("Failed to obtain profile tokens") + + self._set_full_headers() + + # Cache tokens with 24 hour expiry + cache_data = { + "access_token": self.access_token, + "refresh_token": self.refresh_token, + "expires_at": int(datetime.now().timestamp()) + 86400, + "profile_name": profile_name + } + self.cache.get(f"tabii_tokens").set(data=cache_data) + + self.log.info(f"Successfully authenticated as profile '{profile_name}'") + self.log.debug("Authentication tokens cached for 24 hours") + + def _refresh_token(self, refresh_token: str) -> bool: + """Attempt to refresh access token using refresh token""" + + if not refresh_token: + self.log.debug("No refresh token available") + return False + + self.log.debug("Attempting to refresh access token") + + try: + refresh_headers = { + "Content-Type": "application/json; charset=UTF-8", + "Trtdigitalprod": self.config["client"][self.device]["headers"]["Trtdigitalprod"], + "x-clientid": "ANDROID", + "x-clientpass": "000" + } + + response = self.session.post( + url=self.config["endpoints"]["token_refresh"], + json={"refreshToken": refresh_token}, + headers=refresh_headers, + timeout=30 + ) + + if response.status_code != 200: + self.log.warning(f"Token refresh returned HTTP {response.status_code}") + return False + + response_data = response.json() + self.access_token = response_data.get("accessToken") + new_refresh_token = response_data.get("refreshToken") + + if not self.access_token: + self.log.warning("Token refresh response missing access token") + return False + + self.refresh_token = new_refresh_token or refresh_token + self._set_full_headers() + + # Update cache + cache_data = { + "access_token": self.access_token, + "refresh_token": self.refresh_token, + "expires_at": int(datetime.now().timestamp()) + 86400 + } + self.cache.get(f"tabii_tokens").set(data=cache_data) + + self.log.debug("Access token refreshed successfully") + return True + + except Exception as e: + self.log.warning(f"Token refresh failed: {str(e)}") + return False + + def search(self) -> Generator[SearchResult, None, None]: + """Search functionality - not implemented for Tabii""" + self.log.debug("Search called - Tabii does not support search, returning placeholder") + yield SearchResult( + id_=self.title, + title="Direct URL or ID required", + label="N/A", + url=f"https://www.tabii.com/watch/{self.title}" + ) + + def get_titles(self) -> Titles_T: + """Fetch title metadata from Tabii API""" + + match = re.match(self.TITLE_RE, self.title) + if not match: + self.log.error(f"Invalid title format: {self.title}") + raise Exception(f"Invalid title URL or ID format. Expected format: '527983' or 'https://www.tabii.com/watch/527983'") + + title_id = match.group("title_id") + track_id = match.group("track_id") + + self.log.info(f"Fetching metadata for title ID: {title_id}") + if track_id: + self.log.debug(f"Specific episode requested: {track_id}") + + try: + metadata = self.session.get( + url=self.config["endpoints"]["metadata"].format(title_id=title_id), + timeout=30 + ).json() + except Exception as e: + self.log.error(f"Failed to fetch title metadata: {str(e)}") + raise Exception(f"Could not retrieve title information: {str(e)}") + + if "data" not in metadata: + self.log.error("API response missing 'data' field") + raise Exception("Invalid API response format") + + content_type = metadata.get("data", {}).get("contentType", "").lower() + title_name = metadata["data"].get("title", "Unknown") + + self.log.debug(f"Content type: {content_type}, Title: {title_name}") + + if content_type == "movie": + self.movie = True + + if self.movie: + return self._parse_movie(metadata) + else: + return self._parse_series(metadata, track_id) + + def _parse_movie(self, metadata: dict) -> Movies: + """Parse movie metadata""" + + current_content = metadata["data"].get("currentContent", {}) + current_content_id = current_content.get("id") + + if not current_content_id: + self.log.error("Movie content ID not found in metadata") + raise Exception("Could not find movie content ID") + + title = metadata["data"]["title"] + year = int(metadata["data"].get("madeYear", 0)) if metadata["data"].get("madeYear") else None + + self.log.info(f"Parsed movie: {title} ({year})") + self.log.debug(f"Content ID: {current_content_id}") + + return Movies([ + Movie( + id_=current_content_id, + service=self.__class__, + name=title, + description=metadata["data"].get("description", ""), + year=year, + language=Language.get("tr"), + data=current_content + ) + ]) + + def _parse_series(self, metadata: dict, track_id: Optional[str] = None) -> Series: + """Parse series metadata and episodes""" + + episodes = [] + seasons = metadata["data"].get("seasons", []) + series_title = metadata["data"]["title"] + series_year = int(metadata["data"].get("madeYear", 0)) if metadata["data"].get("madeYear") else None + + self.log.info(f"Parsing series: {series_title} ({series_year})") + self.log.debug(f"Found {len(seasons)} season(s)") + + for season in seasons: + season_number = season.get("seasonNumber", 1) + season_episodes = season.get("episodes", []) + + self.log.debug(f"Season {season_number}: {len(season_episodes)} episode(s)") + + for episode in season_episodes: + episode_id = episode["id"] + + # Filter by track_id if specified + if track_id and episode_id != track_id: + continue + + episode_number = int(episode.get("episodeNumber", 0)) + episode_title = episode.get("title", "") + + # Clean episode title (remove leading numbers like "1. Episode Title") + if "." in episode_title: + parts = episode_title.split(".", 1) + if parts[0].strip().isdigit(): + episode_title = parts[1].strip() + + self.log.debug(f"Added S{season_number:02d}E{episode_number:02d}: {episode_title}") + + episodes.append(Episode( + id_=episode_id, + service=self.__class__, + title=series_title, + season=season_number, + number=episode_number, + name=episode_title, + description=episode.get("description", ""), + year=series_year, + language=Language.get("tr"), + data=episode + )) + + if not episodes: + if track_id: + self.log.error(f"Episode with ID {track_id} not found") + raise Exception(f"Episode ID {track_id} not found in series") + else: + self.log.error("No episodes found in series") + raise Exception("No episodes found in series metadata") + + self.log.info(f"Successfully parsed {len(episodes)} episode(s)") + return Series(episodes) + + def get_tracks(self, title: Title_T) -> Tracks: + """Get video, audio, and subtitle tracks for a title""" + + self.log.info(f"Fetching tracks for: {title.name if hasattr(title, 'name') else title.id}") + + # Get media array from title data + media_array = title.data.get("media", []) if hasattr(title, 'data') else [] + + # If not found, try fetching from API + if not media_array: + self.log.debug("Media array not in title data, fetching from API") + media_array = self._fetch_media_array(title) + + if not media_array: + self.log.error("No media information found for title") + raise Exception("No media streams available for this title") + + # Find best available stream + manifest_url, resource_id = self._select_media_stream(media_array) + + if not manifest_url: + self.log.error("No valid media stream found") + raise Exception("No compatible media stream found (Widevine or Clear)") + + # Get DRM ticket if needed + if resource_id: + self.log.debug("Content is DRM-protected, requesting entitlement ticket") + self._request_drm_ticket(title.id, resource_id) + else: + self.log.info("Content is DRM-free") + self.license_url = None + + # Parse DASH manifest + self.log.debug(f"Parsing manifest: {manifest_url}") + try: + tracks = DASH.from_url(url=manifest_url, session=self.session).to_tracks(language=Language.get("tr")) + except Exception as e: + self.log.error(f"Failed to parse manifest: {str(e)}") + raise Exception(f"Could not parse video manifest: {str(e)}") + + # Configure video tracks + for video in tracks.videos: + video.range = Video.Range.SDR + video.closed_captions = False # Prevent ccextractor calls + + self.log.info(f"Found {len(tracks.videos)} video, {len(tracks.audio)} audio, {len(tracks.subtitles)} subtitle track(s)") + + return tracks + + def _fetch_media_array(self, title: Title_T) -> list: + """Fetch media array from API if not in title data""" + + show_id = self.title.split('?')[0] if '?' in self.title else self.title + match = re.match(self.TITLE_RE, show_id) + if match: + show_id = match.group("title_id") + + try: + metadata = self.session.get( + url=self.config["endpoints"]["metadata"].format(title_id=show_id), + timeout=30 + ).json() + except Exception as e: + self.log.error(f"Failed to fetch media metadata: {str(e)}") + return [] + + # Search for episode in metadata + for season in metadata["data"].get("seasons", []): + for episode in season.get("episodes", []): + if episode["id"] == title.id: + self.log.debug(f"Found media array for episode {title.id}") + return episode.get("media", []) + + return [] + + def _select_media_stream(self, media_array: list) -> tuple[Optional[str], Optional[str]]: + """Select best available media stream (prefer Widevine over Clear)""" + + manifest_url = None + resource_id = None + + # Try Widevine first + for media in media_array: + if media.get("drmSchema") == "widevine": + manifest_url = self.config["endpoints"]["playback_manifest"].replace( + "{media_url}", media["url"] + ) + "?width=-1&height=-1&bandwidth=-1&subtitleType=vtt" + resource_id = media.get("resourceId") + self.log.debug(f"Selected Widevine stream, resource ID: {resource_id}") + break + + # Fallback to Clear if no Widevine + if not manifest_url: + for media in media_array: + if media.get("drmSchema") == "clear": + manifest_url = self.config["endpoints"]["playback_manifest"].replace( + "{media_url}", media["url"] + ) + "?width=-1&height=-1&bandwidth=-1&subtitleType=vtt" + self.log.info("Selected DRM-free (Clear) stream") + break + + return manifest_url, resource_id + + def _request_drm_ticket(self, entitlement_id: str, resource_id: str) -> None: + """Request DRM entitlement ticket""" + + try: + ticket_response = self.session.get( + url=self.config["endpoints"]["entitlement_ticket"].format(entitlement_id=entitlement_id), + timeout=30 + ).json() + except Exception as e: + self.log.error(f"Failed to get entitlement ticket: {str(e)}") + raise Exception(f"Could not obtain DRM license ticket: {str(e)}") + + ticket = ticket_response.get("ticket") + if not ticket: + self.log.error("Entitlement ticket not found in response") + raise Exception("Failed to obtain DRM entitlement. Check your subscription status.") + + self.license_url = f"{self.config['endpoints']['widevine_license']}?ticket={ticket}&resource_id={resource_id}" + self.log.debug("DRM license URL configured successfully") + + def get_chapters(self, title: Title_T) -> list[Chapter]: + """Get chapter information - not available for Tabii content""" + return [] + + def get_widevine_license(self, *, challenge: bytes, title: Title_T, track: AnyTrack) -> Optional[Union[bytes, str]]: + """Request Widevine license for DRM-protected content""" + + if not self.license_url: + self.log.error("No license URL available for DRM content") + raise Exception("License URL not configured (content may be DRM-free)") + + self.log.debug(f"Requesting Widevine license for track: {track.id if hasattr(track, 'id') else 'unknown'}") + + try: + response = self.session.post( + url=self.license_url, + data=challenge, + headers={ + "Content-Type": "application/octet-stream", + "User-Agent": self.config["client"][self.device]["license_user_agent"], + "Trtdigitalprod": self.config["client"][self.device]["headers"]["Trtdigitalprod"] + }, + timeout=30 + ) + + response.raise_for_status() + self.log.debug(f"Received license response ({len(response.content)} bytes)") + return response.content + + except Exception as e: + self.log.error(f"License request failed: {str(e)}") + raise Exception(f"Failed to obtain Widevine license: {str(e)}") \ No newline at end of file diff --git a/Tabii/__pycache__/__init__.cpython-312.pyc b/Tabii/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ad5017767aaba8cd03ba68680d594dceb2780906 GIT binary patch literal 32464 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 literal 0 HcmV?d00001 diff --git a/Tabii/config.yaml b/Tabii/config.yaml new file mode 100644 index 0000000..e30115a --- /dev/null +++ b/Tabii/config.yaml @@ -0,0 +1,37 @@ +endpoints: + login: https://eu1.tabii.com/apigateway/auth/v2/login + token_refresh: https://eu1.tabii.com/apigateway/auth/v2/token/refresh + profiles: https://eu1.tabii.com/apigateway/profiles/v2/ + profile_token: https://eu1.tabii.com/apigateway/profiles/v2/{profile_sk}/token + metadata: https://eu1.tabii.com/apigateway/catalog/v1/show/{title_id} + entitlement_ticket: https://eu1.tabii.com/apigateway/entitlement/v1/ticket/{entitlement_id} + widevine_license: https://eu1.tabii.com/apigateway/drm/v1/wv + playback_manifest: https://eu1.tabii.com/apigateway/pbr/v1/media{media_url} + +client: + android: + user_agent: okhttp/4.10.0 + license_user_agent: Dalvik/2.1.0 (Linux; U; Android 8.1.0; Android SDK built for x86 Build/OSM1.180201.037) + device_id: 870d28cdb151d195 + headers: + Accept-Encoding: gzip + Accept-Language: en + Connection: Keep-Alive + Trtdigitalprod: 3cdaf5f04d9449e0b9c82b9a08f1bb1a + x-client-useragent: ANDROID APP Android SDK built for x86 + x-clientid: ANDROID + x-clientpass: "000" + x-uid: 870d28cdb151d195 + App-Bundle-Id: com.trt.tabii.android + App-Name: Tabii + App-Version: "94" + Device-Brand: google + Device-Country: TR + Device-Id: 870d28cdb151d195 + Device-Language: en + Device-Model: sdk_phone_x86 + Device-Name: Google sdk_phone_x86 + Device-OS-Name: "11" + Device-OS-Version: "30" + Device-Type: AndroidPhone + Platform: AndroidPhone \ No newline at end of file