diff --git a/wastream/config/settings.py b/wastream/config/settings.py index f00069a..20b1f98 100644 --- a/wastream/config/settings.py +++ b/wastream/config/settings.py @@ -225,7 +225,7 @@ class Settings(BaseSettings): return { "id": self.ADDON_ID, "name": self.ADDON_NAME, - "version": "3.6.3", + "version": "3.6.2", "description": "Stremio addon to convert DDL to streams via debrid services", "catalogs": [], "resources": ["stream"], diff --git a/wastream/debrid/alldebrid.py b/wastream/debrid/alldebrid.py index 2b59c22..d773f53 100644 --- a/wastream/debrid/alldebrid.py +++ b/wastream/debrid/alldebrid.py @@ -22,10 +22,6 @@ RETRY_ERRORS = [ "REDIRECTOR_ERROR", ] -DEAD_LINK_ERRORS = [ - "LINK_PASS_PROTECTED", -] - # =========================== # AllDebrid Service Class @@ -158,14 +154,6 @@ class AllDebridService(BaseDebridService): debrid_logger.debug(f"[AllDebrid] {error_code}") return "LINK_DOWN" - if error_code in DEAD_LINK_ERRORS: - debrid_logger.debug(f"[AllDebrid] {error_code} - link is password-protected (universally undebridable)") - return "LINK_DOWN" - - if error_code == "NO_SERVER": - debrid_logger.warning(f"[AllDebrid] NO_SERVER - server blocked by AllDebrid (VPN/datacenter detected)") - return "FATAL_ERROR" - if error_code in RETRY_ERRORS: if error_code == "LINK_HOST_UNAVAILABLE": hoster_name = hoster if hoster else urlparse(link).netloc.replace("www.", "") @@ -233,14 +221,6 @@ class AllDebridService(BaseDebridService): debrid_logger.debug(f"[AllDebrid] {error_code2}") return "LINK_DOWN" - if error_code2 in DEAD_LINK_ERRORS: - debrid_logger.debug(f"[AllDebrid] {error_code2} - link is password-protected (universally undebridable)") - return "LINK_DOWN" - - if error_code2 == "NO_SERVER": - debrid_logger.warning(f"[AllDebrid] NO_SERVER - server blocked by AllDebrid (VPN/datacenter detected)") - return "FATAL_ERROR" - if error_code2 in RETRY_ERRORS: if error_code2 == "LINK_HOST_UNAVAILABLE": hoster_name = hoster if hoster else urlparse(link).netloc.replace("www.", "") diff --git a/wastream/debrid/onefichier.py b/wastream/debrid/onefichier.py index 6224065..79aec25 100644 --- a/wastream/debrid/onefichier.py +++ b/wastream/debrid/onefichier.py @@ -1,4 +1,3 @@ -import re import time from asyncio import sleep from typing import Optional, List, Dict @@ -70,8 +69,9 @@ class OneFichierService(BaseDebridService): debrid_logger.error("[1Fichier] Empty API key") return "FATAL_ERROR" - cleaned_link = re.sub(r"[?&]af=[^&]*", "", link) - cleaned_link = cleaned_link.replace("?&", "?").rstrip("?&") + cleaned_link = link + if "&af=" in cleaned_link: + cleaned_link = cleaned_link.split("&af=")[0] debrid_logger.debug("[1Fichier] Converting link") diff --git a/wastream/debrid/premiumize.py b/wastream/debrid/premiumize.py index 2c565e9..5009143 100644 --- a/wastream/debrid/premiumize.py +++ b/wastream/debrid/premiumize.py @@ -11,18 +11,15 @@ from wastream.utils.quality import quality_sort_key # =========================== # Premiumize Error Constants # =========================== -RETRY_CODES = [ - "service_down", - "rate_limit_reached", - "transient_error", - "link_generation_failed", - "semi_permanent_error", - "account_limit_reached", - "service_limit_reached", +RETRY_MESSAGES = [ + "Fair use limit reached!", + "maximum of 25 active downloads" ] -LINK_DOWN_CODES = [ - "not_found", +LINK_DOWN_MESSAGES = [ + "not found", + "not available", + "offline" ] @@ -35,21 +32,22 @@ class PremiumizeService(BaseDebridService): def _handle_api_error( self, - error_code: str, message: str, attempt: int ) -> str: - if error_code in LINK_DOWN_CODES: - debrid_logger.debug(f"[Premiumize] LINK_DOWN: {error_code} - {message}") + message_lower = message.lower() + + if any(kw in message_lower for kw in LINK_DOWN_MESSAGES): + debrid_logger.debug(f"[Premiumize] LINK_DOWN: {message}") return "LINK_DOWN" - if error_code in RETRY_CODES: - debrid_logger.error(f"[Premiumize] RETRY: {error_code} - {message}") + if any(kw in message for kw in RETRY_MESSAGES): + debrid_logger.error(f"[Premiumize] RETRY: {message}") if attempt >= settings.DEBRID_MAX_RETRIES - 1: return "RETRY_ERROR" return "RETRY" - debrid_logger.error(f"[Premiumize] Fatal: {error_code} - {message}") + debrid_logger.error(f"[Premiumize] Fatal: {message}") return "FATAL_ERROR" async def check_cache_and_enrich(self, results: List[Dict], api_key: str, config: Dict, timeout_remaining: float, user_season: Optional[str] = None, user_episode: Optional[str] = None, user_hosts: Optional[List[str]] = None) -> List[Dict]: @@ -111,10 +109,9 @@ class PremiumizeService(BaseDebridService): for attempt in range(settings.DEBRID_MAX_RETRIES): try: - response = await http_client.post( + response = await http_client.get( f"{settings.PREMIUMIZE_API_URL}/transfer/directdl", - headers={"Authorization": f"Bearer {api_key}"}, - data={"src": link}, + params={"apikey": api_key, "src": link}, timeout=settings.HTTP_TIMEOUT ) @@ -152,10 +149,9 @@ class PremiumizeService(BaseDebridService): data = response.json() if data.get("status") != "success": - error_code = data.get("code", "") message = data.get("message", "Unknown error") - api_error_result = self._handle_api_error(error_code, message, attempt) + api_error_result = self._handle_api_error(message, attempt) if api_error_result == "LINK_DOWN": return "LINK_DOWN" elif api_error_result == "FATAL_ERROR": diff --git a/wastream/debrid/torbox.py b/wastream/debrid/torbox.py index 3612d28..dd07ed2 100644 --- a/wastream/debrid/torbox.py +++ b/wastream/debrid/torbox.py @@ -169,15 +169,19 @@ class TorBoxService(BaseDebridService): if not links or not api_key: return links + links.sort(key=quality_sort_key) + top_links = links[:15] + rest_links = links[15:] + cache_timeout = config.get("stream_request_timeout", settings.STREAM_REQUEST_TIMEOUT) check_endpoint = "usenet/checkcached" if download_type == "usenet" else "webdl/checkcached" - cache_logger.debug(f"[TorBox] Checking {len(links)} links (timeout: {cache_timeout}s)") + cache_logger.debug(f"[TorBox] Checking top {len(top_links)} links (timeout: {cache_timeout}s)") start_time = time.time() link_to_hash = {} hashes = [] - for link_dict in links: + for link_dict in top_links: link = link_dict.get("link") if link: if download_type == "usenet": @@ -193,8 +197,10 @@ class TorBoxService(BaseDebridService): hashes.append(link_hash) if not hashes: - cache_logger.debug("[TorBox] No valid links") - return links + cache_logger.debug("[TorBox] No valid links to hash") + for link_dict in rest_links: + link_dict["cache_status"] = "uncached" + return top_links + rest_links http_error_count = 0 @@ -216,25 +222,29 @@ class TorBoxService(BaseDebridService): if response.status_code != 200: cache_logger.error(f"[TorBox] Batch failed: HTTP {response.status_code}") - for link_dict in links: + for link_dict in top_links: link_dict["cache_status"] = "uncached" - return links + for link_dict in rest_links: + link_dict["cache_status"] = "uncached" + return top_links + rest_links response_json = response.json() error_code = response_json.get("error") cooldown_result, http_error_count = await self._handle_cooldown_limit(error_code, http_error_count) if cooldown_result == "RETRY_ERROR": - for link_dict in links: + for link_dict in top_links: link_dict["cache_status"] = "uncached" - return links + for link_dict in rest_links: + link_dict["cache_status"] = "uncached" + return top_links + rest_links elif cooldown_result == "RETRY": continue http_error_count = 0 cache_data = response_json.get("data", {}) - for link_dict in links: + for link_dict in top_links: link = link_dict.get("link") link_hash = link_to_hash.get(link) @@ -280,15 +290,18 @@ class TorBoxService(BaseDebridService): except Exception as e: cache_logger.error(f"[TorBox] Batch error: {type(e).__name__}: {e}") - for link_dict in links: + for link_dict in top_links: link_dict["cache_status"] = "uncached" break - cached_count = sum(1 for r in links if r.get("cache_status") == "cached") - elapsed = time.time() - start_time - cache_logger.debug(f"[TorBox] Done in {elapsed:.1f}s: {cached_count} cached / {len(links) - cached_count} uncached") + for link_dict in rest_links: + link_dict["cache_status"] = "uncached" - return links + cached_count = sum(1 for r in top_links if r.get("cache_status") == "cached") + elapsed = time.time() - start_time + cache_logger.debug(f"[TorBox] Done in {elapsed:.1f}s: {cached_count} cached / {len(top_links) - cached_count} uncached (skipped {len(rest_links)})") + + return top_links + rest_links async def check_cache_and_enrich(self, results: List[Dict], api_key: str, config: Dict, timeout_remaining: float, user_season: Optional[str] = None, user_episode: Optional[str] = None, user_hosts: Optional[List[str]] = None) -> List[Dict]: start_time = time.time() @@ -402,6 +415,8 @@ class TorBoxService(BaseDebridService): for attempt in range(settings.DEBRID_MAX_RETRIES): try: data_payload = {"link": link} + if download_type == "webdl": + data_payload["add_only_if_cached"] = False create_response = await http_client.post( f"{self.API_URL}/{create_endpoint}", @@ -482,6 +497,38 @@ class TorBoxService(BaseDebridService): debrid_logger.error(f"[TorBox] Failed after {settings.DEBRID_MAX_RETRIES} attempts") return "FATAL_ERROR", http_error_count + async def trigger_auto_download(self, link: str, api_key: str): + if not api_key or not link: + return + + download_type = "usenet" if "/nzb/" in link else "webdl" + create_endpoint = "usenet/createusenetdownload" if download_type == "usenet" else "webdl/createwebdownload" + + cleaned_link = link + if download_type == "webdl" and "&af=" in cleaned_link: + cleaned_link = cleaned_link.split("&af=")[0] + elif download_type == "usenet": + nzb_id = link.split("/")[-1] + cleaned_link = f"{settings.DARKI_API_URL}/nzb/{nzb_id}/download" + + try: + headers = self._get_headers(api_key) + data_payload = {"link": cleaned_link} + if download_type == "webdl": + data_payload["add_only_if_cached"] = False + + debrid_logger.info(f"[TorBox] Triggering auto-cacher for uncached link: {cleaned_link[:50]}...") + + # Fire and forget without retries for background job + await http_client.post( + f"{self.API_URL}/{create_endpoint}", + data=data_payload, + headers=headers, + timeout=settings.HTTP_TIMEOUT + ) + except Exception as e: + debrid_logger.error(f"[TorBox] Auto-cacher background trigger failed: {type(e).__name__}: {e}") + async def convert_link(self, link: str, api_key: str, season: Optional[str] = None, episode: Optional[str] = None, hoster: Optional[str] = None) -> Optional[str]: if not api_key: debrid_logger.error("[TorBox] Empty API key") @@ -554,13 +601,7 @@ class TorBoxService(BaseDebridService): debrid_logger.error("[TorBox] mylist failed") return "FATAL_ERROR" - mylist_payload = mylist_data.get("data", {}) - if isinstance(mylist_payload, list): - mylist_item = mylist_payload[0] if mylist_payload else {} - else: - mylist_item = mylist_payload if isinstance(mylist_payload, dict) else {} - - files = mylist_item.get("files", []) + files = mylist_data.get("data", {}).get("files", []) if not files: debrid_logger.error("[TorBox] mylist no files") @@ -596,6 +637,7 @@ class TorBoxService(BaseDebridService): "token": api_key, id_param_key: download_id, "file_id": file_id, + "zip_link": False }, headers=headers, timeout=settings.HTTP_TIMEOUT diff --git a/wastream/main.py b/wastream/main.py index 3d8ac3d..465d1cc 100644 --- a/wastream/main.py +++ b/wastream/main.py @@ -1,9 +1,7 @@ import asyncio -import re import time from contextlib import asynccontextmanager from pathlib import Path -from typing import Optional import uvicorn from fastapi import FastAPI @@ -14,17 +12,13 @@ from starlette.requests import Request from wastream.api.routes import router from wastream.utils.database import setup_database, teardown_database, cleanup_expired_data, rebuild_cache_stats -from wastream.utils.helpers import decode_playback_token from wastream.utils.http_client import http_client from wastream.config.settings import settings -from wastream.utils.logger import setup_logger, addon_logger, api_logger, user_id_var +from wastream.utils.logger import setup_logger, addon_logger, api_logger from wastream.services.health import start_background_health_check from wastream.services.pastebin_scraper import start_pastebin_scraper_loop -UUID_PATTERN = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", re.IGNORECASE) - - # =========================== # Logger Setup # =========================== @@ -53,32 +47,7 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware): class LoguruMiddleware(BaseHTTPMiddleware): - def _extract_user_id(self, request: Request) -> Optional[str]: - path = request.url.path - parts = path.split("/") - - if len(parts) >= 2 and UUID_PATTERN.match(parts[1]): - return parts[1] - - if len(parts) >= 3 and parts[1] == "user" and UUID_PATTERN.match(parts[2]): - return parts[2] - - if len(parts) >= 3 and parts[1] == "playback": - data = decode_playback_token(parts[2]) - if data: - user_uuid = data.get("u") - if user_uuid and UUID_PATTERN.match(user_uuid): - return user_uuid - - if path.startswith("/resolve"): - user_uuid = request.query_params.get("user_uuid") - if user_uuid and UUID_PATTERN.match(user_uuid): - return user_uuid - - return None - async def dispatch(self, request: Request, call_next): - token = user_id_var.set(self._extract_user_id(request)) start_time = time.time() response = None try: @@ -96,7 +65,6 @@ class LoguruMiddleware(BaseHTTPMiddleware): path_parts[2] = "***" safe_path = "/".join(path_parts) api_logger.debug(f"{request.method} {safe_path} - {response.status_code if response else '500'} - {process_time:.2f}s") - user_id_var.reset(token) # =========================== diff --git a/wastream/public/translations.js b/wastream/public/translations.js index d39fec4..84a9ab9 100644 --- a/wastream/public/translations.js +++ b/wastream/public/translations.js @@ -47,6 +47,7 @@ const translations = { cachedOnly: 'Cached links only', enableUsenet: 'Enable Usenet (TorBox only)', fullSeasonPacks: 'Include Full Season Packs (Usenet)', + autoCache: 'Auto-cache best link (TorBox)', deduplicateResults: 'Deduplicate results', earlyStop: 'Early Stop', earlyStopIncludeNzb: 'Include NZB in count', @@ -62,6 +63,7 @@ const translations = { tokenLabel: 'Token:', hostsInfo: 'Select specific hosts or leave empty for all', sourcesInfo: 'Select specific sources or leave empty for all', + autoCacheInfo: 'Automatically triggers a background download for the best quality link if no streams are cached.', timeoutInfo: 'Maximum search time (default: 20s)', excludedInfo: 'Streams with these words will be filtered', deduplicateInfo: 'Hides duplicate results from different hosts', @@ -179,6 +181,7 @@ const translations = { cachedOnly: 'Liens en cache uniquement', enableUsenet: 'Activer Usenet (TorBox uniquement)', fullSeasonPacks: 'Inclure les packs de saison (Usenet)', + autoCache: 'Auto-cache le meilleur lien (TorBox)', deduplicateResults: 'Dédupliquer les résultats', earlyStop: 'Arrêt Anticipé', earlyStopIncludeNzb: 'Inclure NZB dans le comptage', @@ -194,6 +197,7 @@ const translations = { tokenLabel: 'Token :', hostsInfo: 'Sélectionner des hébergeurs ou laisser vide pour tous', sourcesInfo: 'Sélectionner des sources ou laisser vide pour toutes', + autoCacheInfo: 'Déclenche automatiquement le téléchargement en arrière-plan du meilleur lien si aucun stream n\'est en cache.', timeoutInfo: 'Temps de recherche maximum (défaut : 20s)', excludedInfo: 'Les streams avec ces mots seront filtrés', deduplicateInfo: 'Masque les résultats identiques provenant de différents hébergeurs', diff --git a/wastream/services/stream.py b/wastream/services/stream.py index 0bc1c8e..94e5fcd 100644 --- a/wastream/services/stream.py +++ b/wastream/services/stream.py @@ -475,6 +475,22 @@ class StreamService: stream_logger.debug(f"Excluded {excluded_count} streams") streams = filtered_streams + torbox_config = next((s for s in get_debrid_services(config) if s.get("service") == "torbox"), None) + if torbox_config and torbox_config.get("auto_cache", False): + cached_count = sum(1 for s in streams if "⚡" in s["name"] and "TB" in s["name"]) + + if cached_count == 0 and len(streams) > 0: + best_torbox_stream = next((s for s in streams if "TB" in s["name"]), None) + if best_torbox_stream: + token_data = decode_playback_token(best_torbox_stream["url"].split("/")[-2]) + link_to_download = token_data.get("l") + + if link_to_download: + asyncio.create_task(torbox_service.trigger_auto_download( + link=link_to_download, + api_key=torbox_config.get("api_key") + )) + return streams async def _get_metadata(self, imdb_id: str, tmdb_api_token: str) -> Optional[Dict]: @@ -1286,7 +1302,23 @@ class StreamService: excluded_count = len(streams) - len(filtered_streams) if excluded_count > 0: stream_logger.debug(f"Excluded {excluded_count} streams") - return filtered_streams + streams = filtered_streams + + torbox_config = next((s for s in get_debrid_services(config) if s.get("service") == "torbox"), None) + if torbox_config and torbox_config.get("auto_cache", False): + cached_count = sum(1 for s in streams if "⚡" in s["name"] and "TB" in s["name"]) + + if cached_count == 0 and len(streams) > 0: + best_torbox_stream = next((s for s in streams if "TB" in s["name"]), None) + if best_torbox_stream: + token_data = decode_playback_token(best_torbox_stream["url"].split("/")[-2]) + link_to_download = token_data.get("l") + + if link_to_download: + asyncio.create_task(torbox_service.trigger_auto_download( + link=link_to_download, + api_key=torbox_config.get("api_key") + )) return streams diff --git a/wastream/templates/dashboard.html b/wastream/templates/dashboard.html index fcc8729..9c2fb6a 100644 --- a/wastream/templates/dashboard.html +++ b/wastream/templates/dashboard.html @@ -1818,17 +1818,6 @@ min-width: 70px; } - .log-user-id { - padding: 2px 8px; - border-radius: var(--radius-sm); - font-size: 11px; - font-weight: 500; - flex-shrink: 0; - background: rgba(250, 204, 21, 0.15); - color: #facc15; - font-family: var(--font-mono, monospace); - } - .log-context { padding: 2px 8px; border-radius: var(--radius-sm); @@ -4197,11 +4186,7 @@ if (filters.context && log.context !== filters.context) return false; if (filters.level && log.level !== filters.level) return false; if (filters.prefix && !log.message.includes(`[${filters.prefix}]`)) return false; - if (filters.search) { - const inMessage = log.message.toLowerCase().includes(filters.search); - const inUserId = log.user_id && log.user_id.toLowerCase().includes(filters.search); - if (!inMessage && !inUserId) return false; - } + if (filters.search && !log.message.toLowerCase().includes(filters.search)) return false; return true; } @@ -4237,16 +4222,11 @@ const entry = document.createElement('div'); entry.className = 'log-entry'; - const userIdBadge = log.user_id - ? `👤 ${escapeHtml(log.user_id.substring(0, 8))}` - : ''; - entry.innerHTML = `