From 8f115881e2e30b664cc78db6e4b8333cd4ce4c22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=88=9A=28noham=29=C2=B2?= <100566912+NohamR@users.noreply.github.com> Date: Tue, 26 May 2026 17:21:54 +0200 Subject: [PATCH] Cache tokens and improve Oqee login flow Add local token caching and validation for Oqee client, including _is_token_valid, load_cache and save_cache using .oqee_token.json to reuse valid JWTs and avoid unnecessary re-authentication. Update login() to try loading cache first, keep existing credential/IP login flows with clearer logging and improved fallback/error handling, and save tokens after successful auth. Also add required imports (json, os, time) and minor logging tweak in utils/downloader.py to include the Widevine CDM device filename in the fetch message. --- utils/downloader.py | 3 +- utils/oqee.py | 114 ++++++++++++++++++++++++++++++++------------ 2 files changed, 86 insertions(+), 31 deletions(-) diff --git a/utils/downloader.py b/utils/downloader.py index 4dcc16b..2b2fbe3 100644 --- a/utils/downloader.py +++ b/utils/downloader.py @@ -51,7 +51,8 @@ def get_keys(kids: list[str], method: dict) -> list[str]: keys.append(f"{kid}:{key}") return keys - logger.info("Fetching DRM keys via Widevine CDM...") + cdm_name = (method["device_file"]).split("/")[-1] + logger.info(f"Fetching DRM keys via Widevine CDM {cdm_name}...") client = method["client_class"] device = Device.load(method["device_file"]) diff --git a/utils/oqee.py b/utils/oqee.py index 7df10a7..01e16e2 100644 --- a/utils/oqee.py +++ b/utils/oqee.py @@ -1,6 +1,9 @@ """OQEE streaming service client for authentication and content access.""" import base64 +import json +import os +import time from urllib.parse import urlparse, parse_qs from bs4 import BeautifulSoup import requests @@ -92,6 +95,50 @@ class OqeeClient: # pylint: disable=too-many-instance-attributes logger.info("Logging in") self.login(username, password) + def _is_token_valid(self, token): + """Check if JWT token is still valid (checking expiry).""" + if not token: + return False + try: + payload_b64 = token.split(".")[1] + payload_b64 += "=" * (-len(payload_b64) % 4) + payload = json.loads(base64.b64decode(payload_b64)) + return payload.get("exp", 0) > time.time() + except Exception: + return False + + def load_cache(self): + """Load tokens from cache file.""" + if os.path.exists(".oqee_token.json"): + try: + with open(".oqee_token.json", "r", encoding="utf-8") as f: + cache = json.load(f) + if self._is_token_valid( + cache.get("access_token") + ) and self._is_token_valid(cache.get("right_token")): + self.access_token = cache.get("access_token") + self.right_token = cache.get("right_token") + self.profil_id = cache.get("profil_id") + return True + except Exception as e: + logger.debug("Failed to load token cache: %s", e) + return False + + def save_cache(self): + """Save tokens to cache file.""" + try: + with open(".oqee_token.json", "w", encoding="utf-8") as f: + json.dump( + { + "access_token": self.access_token, + "right_token": self.right_token, + "profil_id": self.profil_id, + }, + f, + ) + except Exception as e: + logger.debug("Failed to save token cache: %s", e) + def _build_headers(self, overrides=None, remove=None): """Clone default headers and apply optional overrides/removals.""" headers = self._headers_template.copy() @@ -338,42 +385,49 @@ class OqeeClient: # pylint: disable=too-many-instance-attributes """ Log in to the Oqee service and set up necessary tokens and headers. """ - if not username or not password: - logger.info("No credentials provided, using IP login.") - self.access_token = self.login_ip() + if self.load_cache(): + logger.info("Loaded valid tokens from cache.") else: - try: - if self.abo: - if self.fbx: - logger.info( - "Logging in with provided credentials (abo account detected)." - ) - self.access_token = self.login_cred_fbx(username, password) + if not username or not password: + logger.info("No credentials provided, using IP login.") + self.access_token = self.login_ip() + else: + try: + if self.abo: + if self.fbx: + logger.info( + "Logging in with provided credentials (abo account detected)." + ) + self.access_token = self.login_cred_fbx(username, password) + else: + logger.info( + "Logging in with provided credentials (abo account detected, non-FBX)." + ) + self.access_token = self.login_cred_mobile( + username, password + ) else: logger.info( - "Logging in with provided credentials (abo account detected, non-FBX)." + "Logging in with provided credentials (free account detected)." ) - self.access_token = self.login_cred_mobile(username, password) - else: - logger.info( - "Logging in with provided credentials (free account detected)." + self.access_token = self.login_cred_free(username, password) + except ValueError as e: + logger.warning( + "Credential login failed: %s. Falling back to IP login.", e ) - self.access_token = self.login_cred_free(username, password) - except ValueError as e: - logger.warning( - "Credential login failed: %s. Falling back to IP login.", e - ) - self.access_token = self.login_ip() + self.access_token = self.login_ip() - logger.info("Fetching rights token") - self.right_token = self.right() - logger.debug( - "Rights token obtained: %s", - self.right_token[:10] + "..." if self.right_token else "None", - ) - logger.info("Fetching profile ID") - self.profil_id = self.profil() - logger.debug("Profile ID obtained: %s", self.profil_id) + logger.info("Fetching rights token") + self.right_token = self.right() + logger.debug( + "Rights token obtained: %s", + self.right_token[:10] + "..." if self.right_token else "None", + ) + logger.info("Fetching profile ID") + self.profil_id = self.profil() + logger.debug("Profile ID obtained: %s", self.profil_id) + + self.save_cache() self.headers = self._build_headers( overrides={