diff --git a/services/TFC/__init__.py b/services/TFC/__init__.py index 59f6298..e8043e1 100644 --- a/services/TFC/__init__.py +++ b/services/TFC/__init__.py @@ -2,6 +2,7 @@ import json import re import time import sys +import os from datetime import datetime, timedelta from typing import Union, Generator, Optional from urllib.parse import urljoin @@ -14,6 +15,7 @@ from devine.core.constants import AnyTrack from devine.core.service import Service from devine.core.titles import Episode, Movie, Movies, Series from devine.core.tracks import Tracks, Chapters, Subtitle, Chapter +from devine.core.tracks.attachment import Attachment from devine.core.credential import Credential from devine.core.search_result import SearchResult from devine.core.downloaders import curl_impersonate @@ -22,7 +24,6 @@ from devine.core.config import config from devine.core.manifests.dash import DASH import warnings -# Weird chunk error from search, we're using this to ignore the warning popup warnings.filterwarnings("ignore", message="chunk_size is ignored") @@ -82,23 +83,18 @@ class TFC(Service): headers=headers, ) - # Parse the authentication response response_json = auth_response.json() - # Check if authentication was successful if response_json.get("status") == "OK" and "UserAuthentication" in response_json: - # Extract token from UserAuthentication self.token = response_json["UserAuthentication"] self.refresh_token = response_json["refreshToken"] self.token_expiry = (datetime.now() + timedelta(minutes=4)).timestamp() - # Update session headers with the Authorization token self.session.headers.update({"Authorization": f"Bearer {self.token}"}) else: - # Retry login if the first attempt fails - if auth_response.status_code == 401: # Assuming 401 for unauthorized + if auth_response.status_code == 401: print("First login attempt failed, retrying...") - return self.authenticate(cookies, credential) # Recursive retry + return self.authenticate(cookies, credential) else: raise ValueError("Failed to authenticate. Response was not as expected.") @@ -124,16 +120,13 @@ class TFC(Service): if not title: continue - # Get detailed metadata detail_url = self.config["endpoints"]["api_playback"].format(js=self.get_js_value(), id=result["objectID"]) detail_response = self.session.get(detail_url) detail_data = detail_response.json() - # Extract description and media type description = detail_data.get("description", {}).get("en", "")[:200] + "..." media_type = "TV" if "children" in detail_data else "Movie" - # Extract year and episode count for TV shows year = detail_data.get("release_year") episode_count = 0 @@ -142,14 +135,12 @@ class TFC(Service): [episode for episode in detail_data.get("children", []) if "-tlr" not in episode["id"]] ) - # Construct label with episode count for TV shows label = media_type if year: label += f" ({year})" if media_type == "TV": label += f" {episode_count} Episode{'' if episode_count == 1 else 's'}" - # Create SearchResult with additional details yield SearchResult( id_=result["objectID"], title=title, @@ -158,7 +149,6 @@ class TFC(Service): ) def get_js_value(self) -> Optional[str]: - # Simulate browsing to the page and download the HTML file for _ in curl_impersonate( urls="https://www.iwanttfc.com/#!/browse", output_dir=config.directories.temp, @@ -166,12 +156,10 @@ class TFC(Service): ): pass - # Read the downloaded HTML file html_path = config.directories.temp / "browse_page.html" with html_path.open("r", encoding="utf8") as f: html_content = f.read() - # Find the script tag with the catalog URL and extract the 'js' value match = re.search(r'src="https://absprod-static.iwanttfc.com/c/6/catalog/(.*?)/script.js', html_content) if match: return match.group(1) @@ -179,7 +167,6 @@ class TFC(Service): return None def get_titles(self) -> Union[Movies, Series]: - # Get title metadata try: title_metadata = requests.get( self.config["endpoints"]["api_playback"].format(js=self.get_js_value(), id=self.title) @@ -188,10 +175,9 @@ class TFC(Service): self.log.warning("Show title does not exist.") sys.exit(1) - # Check for GEOFENCE rules (this part remains the same) rules = title_metadata.get("rules", {}).get("rules", []) for rule in rules: - if rule.get("start") <= time.time() * 1000 <= rule.get("end"): # Check if rule is active + if rule.get("start") <= time.time() * 1000 <= rule.get("end"): required_countries = rule.get("countries", []) if required_countries: current_region = get_ip_info(self.session)["country"].lower() @@ -203,19 +189,16 @@ class TFC(Service): sys.exit(0) if "children" in title_metadata: - # TV Show - Extract episodes with correct season info episodes = [] for episode in title_metadata.get("children", []): episode_id = episode["id"] - # Extract season and episode number from ID match = re.match(r".*-s(\d+)e(\d+)$", episode_id, re.IGNORECASE) if not match: - continue # Skip if unable to parse season and episode + continue season, number = map(int, match.groups()) - # Create Episode object with season and episode number episode_obj = Episode( id_=episode_id, title=title_metadata.get("title", {}).get("en"), @@ -229,11 +212,9 @@ class TFC(Service): return Series(episodes) else: - # Movie - Extract movie details movie_name = title_metadata.get("title", {}).get("en") movie_year = title_metadata.get("release_year") - # Create Movie object movie_class = Movie( id_=self.title, name=movie_name, @@ -244,17 +225,16 @@ class TFC(Service): return Movies([movie_class]) def get_tracks(self, title: Union[Movie, Episode]) -> Tracks: - if isinstance(title, Episode) and not title.data: - # Fetch detailed episode data if needed + if not title.data: episode_data = requests.get( self.config["endpoints"]["api_playback"].format(js=self.get_js_value(), id=title.id) ).json() title.data = episode_data + else: + episode_data = title.data - # Extract MPD URLs mpd_urls = episode_data.get("media", {}).get("mpds", []) - # Extract subtitle URLs and languages subtitle_data = [ ( urljoin(self.config["endpoints"]["api_subtitle"], caption.get("id")) + ".vtt", @@ -265,12 +245,11 @@ class TFC(Service): tracks = Tracks() - # Create Video and Audio Tracks from MPDs, avoiding duplicates and storing episode_id for mpd_url in mpd_urls: mpd_tracks = DASH.from_url(url=mpd_url, session=self.session).to_tracks(language=title.language or "fil") for track in mpd_tracks: if not tracks.exists(by_id=track.id): - track.data["episode_id"] = episode_data.get("id") # Store episode_id in track.data + track.data["episode_id"] = episode_data.get("id") tracks.add(track) for track in tracks.audio: @@ -280,7 +259,6 @@ class TFC(Service): track.language._dict = {"language": mpd_lang} track.language._str_tag = mpd_lang - # Create Subtitle Tracks for all languages, avoiding duplicates for subtitle_url, language in subtitle_data: subtitle_track = Subtitle( id_=subtitle_url.split("/")[-1].split(".")[0], @@ -296,6 +274,37 @@ class TFC(Service): chapters = self.get_chapters(title) tracks.chapters = Chapters(chapters) + thumbnail_id = episode_data.get("thumbnail") or episode_data.get("poster") or episode_data.get("thumb") + if not thumbnail_id: + images = episode_data.get("images", []) + if images: + thumbnail_data = images[0] + thumbnail_id = thumbnail_data.get("id") or thumbnail_data.get("url").split("/")[-1].split(".")[0] + + if thumbnail_id: + thumbnail_base_url = self.config["endpoints"]["api_thumbnail"] + thumbnail_url = f"{thumbnail_base_url}{thumbnail_id}.jpg" + thumbnail_response = self.session.get(thumbnail_url) + if thumbnail_response.status_code == 200: + thumbnail_filename = f"{title.id}_thumbnail.jpg" + thumbnail_path = config.directories.temp / thumbnail_filename + + os.makedirs(config.directories.temp, exist_ok=True) + + with open(thumbnail_path, "wb") as f: + f.write(thumbnail_response.content) + + thumbnail_attachment = Attachment( + path=thumbnail_path, + name=thumbnail_filename, + mime_type="image/jpeg", + description="Thumbnail", + ) + + tracks.attachments.append(thumbnail_attachment) + else: + self.log.warning("Thumbnail not found for title.") + return tracks def get_chapters(self, title: Union[Movie, Episode]) -> list[Chapter]: diff --git a/services/VIU/__init__.py b/services/VIU/__init__.py index 1f28683..5f472ea 100644 --- a/services/VIU/__init__.py +++ b/services/VIU/__init__.py @@ -31,7 +31,7 @@ class VIU(Service): 1 & 2 has different api Author: unnamed improved by @sp4rk.y - last update: 17/09/2024 + last update: 18/09/2024 """ # GEOFENCE = ("sg",) @@ -67,11 +67,7 @@ class VIU(Service): credential: Optional[Credential] = None, ) -> None: self.credentials = credential - self.session.headers.update( - { - "Referer": "https://viu.com/" # headers Origin make 403 error - } - ) + self.session.headers.update({"Referer": "https://viu.com/"}) self.log.info(" + Downloading without an account...") self.log.info(f" + Detected using: {self.jenis}")