diff --git a/pyproject.toml b/pyproject.toml index 91dcd84..5735f51 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ authors = [ dependencies = [ "aiohttp>=3.13.2", + "bs4>=0.0.2", "InquirerPy>=0.3.4", "python-dotenv>=1.2.1", "pywidevine>=1.9.0", diff --git a/utils/oqee.py b/utils/oqee.py index d4b73ca..7df10a7 100644 --- a/utils/oqee.py +++ b/utils/oqee.py @@ -2,6 +2,7 @@ import base64 from urllib.parse import urlparse, parse_qs +from bs4 import BeautifulSoup import requests from dotenv import load_dotenv @@ -31,15 +32,6 @@ class OqeeClient: # pylint: disable=too-many-instance-attributes ), "accept-language": "en-GB,en-US;q=0.9,en;q=0.8", "cache-control": "no-cache", - "pragma": "no-cache", - "priority": "u=0, i", - "sec-ch-ua": '"Not(A:Brand";v="8", "Chromium";v="144", "Google Chrome";v="144"', - "sec-ch-ua-mobile": "?0", - "sec-ch-ua-platform": '"macOS"', - "sec-fetch-dest": "document", - "sec-fetch-mode": "navigate", - "sec-fetch-site": "none", - "sec-fetch-user": "?1", "upgrade-insecure-requests": "1", "user-agent": ( "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " @@ -61,6 +53,10 @@ class OqeeClient: # pylint: disable=too-many-instance-attributes self.lic_url = "https://license.oqee.net/api/v1/live/license/widevine" if "fbx" in username.lower(): self.abo = True + self.fbx = True + elif len(username) == 8 and username.isdigit(): + self.abo = True + self.fbx = False else: self.abo = False @@ -131,7 +127,7 @@ class OqeeClient: # pylint: disable=too-many-instance-attributes # logger.info("Selecting first profile by default.") return data["result"][0]["id"] - def login_cred_abo(self, username, password): + def login_cred_fbx(self, username, password): """Authenticate with OQEE service using Free account credentials.""" headers = self._build_headers( overrides={ @@ -169,20 +165,12 @@ class OqeeClient: # pylint: disable=too-many-instance-attributes "Content-Type": "application/x-www-form-urlencoded", "Origin": "https://subscribe.free.fr", "Referer": "https://subscribe.free.fr/auth/auth.pl?", - "Sec-Fetch-Dest": "document", - "Sec-Fetch-Mode": "navigate", - "Sec-Fetch-Site": "same-origin", - "Sec-Fetch-User": "?1", - "Sec-GPC": "1", "Upgrade-Insecure-Requests": "1", "user-agent": ( "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/131.0.0.0 Safari/537.36" ), - "sec-ch-ua": '"Brave";v="123", "Not:A-Brand";v="8", "Chromium";v="123"', - "sec-ch-ua-mobile": "?0", - "sec-ch-ua-platform": '"macOS"', } data = { "login": username, @@ -214,6 +202,75 @@ class OqeeClient: # pylint: disable=too-many-instance-attributes ).json() return data["result"]["token"] + def login_cred_mobile(self, username, password): + """Authenticate with OQEE service using Free mobile account credentials.""" + headers = { + "accept": "*/*", + "accept-language": "en-GB,en-US;q=0.9,en;q=0.8", + "cache-control": "no-cache", + "content-type": "application/json", + "origin": "https://tv.free.fr", + "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36", + "x-oqee-customization": "0", + "x-oqee-platform": "web", + } + + data = {"provider": "freemobile", "platform": "web"} + response = self.session.post( + "https://api.oqee.net/api/v2/user/oauth/init", headers=headers, json=data + ).json() + redirect_url = response["result"]["redirect_url"] + token = parse_qs(urlparse(redirect_url).query)["token"][0] + + headers = { + "Content-Type": "application/x-www-form-urlencoded", + "Origin": "https://mobile.free.fr", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36", + } + + page = self.session.get(redirect_url, headers=headers) + soup = BeautifulSoup(page.text, "html.parser") + + token_csrf = soup.find("input", {"name": "token-csrf"}) + token_csrf_value = token_csrf["value"] if token_csrf else None + callback_url = soup.find("input", {"name": "callback_url"}) + callback_url_value = callback_url["value"] if callback_url else None + + params = ( + ("service", "oqee"), + ("token", token), + ) + + data = { + "token-csrf": token_csrf_value, + "callback_url": callback_url_value, + "identifiant": username, + "password": password, + "action": "sso-submit", + } + + response = self.session.post( + "https://mobile.free.fr/sso/", headers=headers, params=params, data=data + ) + + parsed_url = parse_qs(urlparse(response.url).query) + if "result" not in parsed_url: + raise ValueError( + "Login failed: invalid credentials or error in authentication" + ) + freemobile_token = parsed_url["result"][0] + + headers = self._build_headers( + overrides={"x-oqee-customization": "0"}, remove=("x-oqee-account-provider",) + ) + data = self.session.post( + "https://api.oqee.net/api/v5/user/login", + headers=headers, + json={"type": "freemobileoa", "token": freemobile_token}, + ).json() + return data["result"]["token"] + def login_cred_free(self, username, password): """Authenticate with OQEE service using Free account credentials.""" headers = { @@ -222,15 +279,6 @@ class OqeeClient: # pylint: disable=too-many-instance-attributes "cache-control": "no-cache", "content-type": "application/json", "origin": "https://tv.free.fr", - "pragma": "no-cache", - "priority": "u=1, i", - "referer": "https://tv.free.fr/", - "sec-ch-ua": '"Chromium";v="146", "Not-A.Brand";v="24", "Google Chrome";v="146"', - "sec-ch-ua-mobile": "?0", - "sec-ch-ua-platform": '"macOS"', - "sec-fetch-dest": "empty", - "sec-fetch-mode": "cors", - "sec-fetch-site": "cross-site", "user-agent": ( "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36" @@ -240,7 +288,6 @@ class OqeeClient: # pylint: disable=too-many-instance-attributes } data = {"provider": "oqee", "platform": "web"} - response = self.session.post( "https://api.oqee.net/api/v2/user/oauth/init", headers=headers, json=data ).json() @@ -297,10 +344,16 @@ class OqeeClient: # pylint: disable=too-many-instance-attributes else: try: if self.abo: - logger.info( - "Logging in with provided credentials (abo account detected)." - ) - self.access_token = self.login_cred_abo(username, password) + 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 (free account detected)." diff --git a/uv.lock b/uv.lock index 3ed764d..15d9b7d 100644 --- a/uv.lock +++ b/uv.lock @@ -217,6 +217,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, ] +[[package]] +name = "beautifulsoup4" +version = "4.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, +] + +[[package]] +name = "bs4" +version = "0.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/aa/4acaf814ff901145da37332e05bb510452ebed97bc9602695059dd46ef39/bs4-0.0.2.tar.gz", hash = "sha256:a48685c58f50fe127722417bae83fe6badf500d54b55f7e39ffe43b798653925", size = 698, upload-time = "2024-01-17T18:15:47.371Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/bb/bf7aab772a159614954d84aa832c129624ba6c32faa559dfb200a534e50b/bs4-0.0.2-py2.py3-none-any.whl", hash = "sha256:abf8742c0805ef7f662dce4b51cca104cffe52b835238afc169142ab9b3fbccc", size = 1189, upload-time = "2024-01-17T18:15:48.613Z" }, +] + [[package]] name = "certifi" version = "2025.11.12" @@ -758,6 +783,7 @@ version = "0.1.0" source = { editable = "." } dependencies = [ { name = "aiohttp" }, + { name = "bs4" }, { name = "inquirerpy" }, { name = "python-dotenv" }, { name = "pywidevine" }, @@ -775,6 +801,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "aiohttp", specifier = ">=3.13.2" }, + { name = "bs4", specifier = ">=0.0.2" }, { name = "inquirerpy", specifier = ">=0.3.4" }, { name = "python-dotenv", specifier = ">=1.2.1" }, { name = "pywidevine", specifier = ">=1.9.0" }, @@ -1194,6 +1221,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] +[[package]] +name = "soupsieve" +version = "2.8.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/2c/0a5f6f8ee0d5589e48c7640213ed5175d52cf540a06725b628cc1a45d6ce/soupsieve-2.8.4.tar.gz", hash = "sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e", size = 121110, upload-time = "2026-05-24T13:55:57.154Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65", size = 37304, upload-time = "2026-05-24T13:55:55.406Z" }, +] + [[package]] name = "tabulate" version = "0.9.0"