Add Free Mobile SSO login

Add support for Free Mobile SSO authentication by implementing login_cred_mobile which parses the SSO page with BeautifulSoup and exchanges the resulting token for an OQEE access token. Rename login_cred_abo to login_cred_fbx, introduce a self.fbx flag (and mark 8-digit numeric usernames as abo/non-FBX), and update the login selection logic to call the appropriate handler (FBX vs mobile). Also import BeautifulSoup, simplify some request headers, add bs4 to pyproject.toml, and update the lockfile accordingly.
This commit is contained in:
√(noham)²
2026-05-26 17:07:48 +02:00
parent 96fa5c07c4
commit a1dfa082d7
3 changed files with 122 additions and 32 deletions

View File

@@ -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",

View File

@@ -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)."

36
uv.lock generated
View File

@@ -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"