implement plexamp headless claim endpoint

This commit is contained in:
Mathieu Broillet 2025-08-24 22:56:19 +02:00
parent 45f0756191
commit a7492c152d
Signed by: mathieub
GPG Key ID: 4428608CDA3A98D3
3 changed files with 153 additions and 4 deletions

View File

@ -5,9 +5,13 @@ subscription.
### Requirements ### Requirements
- A router that can redirect traffic (i.e. OPNsense, pfSense, DD-WRT...) - A router that **can redirect traffic** (i.e. OPNsense, pfSense, DD-WRT...)
- (alternative) a DNS server that can redirect traffic *(some apps won't work due to DNS pinning)* - _(alternative) a DNS server that can redirect traffic (some apps won't work due to DNS pinning)_
- A reverse proxy (i.e. Traefik, Nginx, Caddy...) - A reverse proxy (i.e. Traefik, Nginx, Caddy...)
- A Plex server (self-hosted)
### What works?
- PlexAmp mobile (download mode)
## How to setup ? ## How to setup ?

108
const.py
View File

@ -1,4 +1,5 @@
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
import xml.etree.ElementTree as ET
OFFICIAL_API = "https://clients.plex.tv" OFFICIAL_API = "https://clients.plex.tv"
@ -299,6 +300,113 @@ PLEXAMP_FEATURES = [
"webhooks" "webhooks"
] ]
PLEXAMP_SUBSCRIPTION_XML = ET.fromstring("""
<subscription active="1" status="Active" plan="">
<feature id="guided-upgrade" />
<feature id="increase-password-complexity" />
<feature id="upgrade-3ds2" />
<feature id="ad-countdown-timer" />
<feature id="adaptive_bitrate" />
<feature id="amazon-loop-debug" />
<feature id="Android - Dolby Vision" />
<feature id="Android - PiP" />
<feature id="avod-ad-analysis" />
<feature id="avod-new-media" />
<feature id="blacklist_get_signin" />
<feature id="camera_upload" />
<feature id="CU Sunset" />
<feature id="client-radio-stations" />
<feature id="cloudsync" />
<feature id="cloudflare-turnstile-required" />
<feature id="common-sense-media-ratings-premium" />
<feature id="comments_and_replies_push_notifications" />
<feature id="friend_request_push_notifications" />
<feature id="community_access_plex_tv" />
<feature id="community_friends_group_notifications" />
<feature id="companions_sonos" />
<feature id="content_filter" />
<feature id="custom-home-removal" />
<feature id="grandfather-sync" />
<feature id="disable-facebook-auth" />
<feature id="disable_home_user_friendships" />
<feature id="disable_sharing_friendships" />
<feature id="downloads-gating" />
<feature id="drm_support" />
<feature id="dvr" />
<feature id="dvr-block-unsupported-countries" />
<feature id="le_isrg_root_x1" />
<feature id="epg-recent-channels" />
<feature id="federated-auth" />
<feature id="global-continue-watching" />
<feature id="hwtranscode" />
<feature id="hardware_transcoding" />
<feature id="home" />
<feature id="HRK_enable_EUR" />
<feature id="imagga-v2" />
<feature id="ios14-privacy-banner" />
<feature id="item_clusters" />
<feature id="iterable-notification-tokens" />
<feature id="keep-payment-method" />
<feature id="kevin-bacon" />
<feature id="lets_encrypt" />
<feature id="lightning-dvr-pivot" />
<feature id="livetv" />
<feature id="allow_dvr" />
<feature id="live-tv-support-incomplete-segments" />
<feature id="tuner-sharing" />
<feature id="lyrics" />
<feature id="metadata_search" />
<feature id="vod_cloudflare" />
<feature id="music_videos" />
<feature id="new_plex_pass_prices" />
<feature id="news-provider-sunset-modal" />
<feature id="nominatim" />
<feature id="pass" />
<feature id="photos-favorites" />
<feature id="photos-metadata-edition" />
<feature id="photosV6-edit" />
<feature id="photosV6-tv-albums" />
<feature id="pms_health" />
<feature id="premium-dashboard" />
<feature id="premium_music_metadata" />
<feature id="rate-limit-client-token" />
<feature id="reactions_push_notifications_settings" />
<feature id="shared_server_notification" />
<feature id="shared_source_notification" />
<feature id="redirect-subscription-to-account-page" />
<feature id="scrobbling-service-plex-tv" />
<feature id="album-types" />
<feature id="collections" />
<feature id="music-analysis" />
<feature id="radio" />
<feature id="session_bandwidth_restrictions" />
<feature id="session_kick" />
<feature id="exclude restrictions" />
<feature id="signin_notification" />
<feature id="signin_with_apple" />
<feature id="skip-data-licensing-consent" />
<feature id="sleep-timer" />
<feature id="spring_serve_ad_provider" />
<feature id="sync" />
<feature id="trailers" />
<feature id="transcoder_cache" />
<feature id="boost-voices" />
<feature id="TREBLE-show-features" />
<feature id="silence-removal" />
<feature id="sweet-fades" />
<feature id="visualizers" />
<feature id="volume-leveling" />
<feature id="two-factor-authentication" />
<feature id="unsupportedtuners" />
<feature id="plexpass_from_billing_context" />
<feature id="vod-schema" />
<feature id="watch-together-invite" />
<feature id="watchlist-rss" />
<feature id="web_server_dashboard" />
<feature id="webhooks" />
</subscription>
""")
ENTITLEMENTS = ["deprecated_google_iap_activation", "all", "roku", "android", "xbox_one", "xbox_360", "windows", ENTITLEMENTS = ["deprecated_google_iap_activation", "all", "roku", "android", "xbox_one", "xbox_360", "windows",
"windows_phone", "ios"] "windows_phone", "ios"]

View File

@ -8,7 +8,7 @@ from starlette.responses import JSONResponse
from client import AsyncCustomHost, NameSolver from client import AsyncCustomHost, NameSolver
from const import OFFICIAL_API, FEATURES_DICT, ENTITLEMENTS, CURRENT_DATE_MINUS_ONE_DAY, FEATURES, \ from const import OFFICIAL_API, FEATURES_DICT, ENTITLEMENTS, CURRENT_DATE_MINUS_ONE_DAY, FEATURES, \
CURRENT_DATE_MINUS_ONE_DAY_Z, NEXT_MONTH_DATE_Z, PLEXAMP_FEATURES, TIMESTAMP_CURRENT_PLUS_30DAYS, \ CURRENT_DATE_MINUS_ONE_DAY_Z, NEXT_MONTH_DATE_Z, PLEXAMP_FEATURES, TIMESTAMP_CURRENT_PLUS_30DAYS, \
TIMESTAMP_CURRENT_MINUS_30MIN TIMESTAMP_CURRENT_MINUS_30MIN, PLEXAMP_SUBSCRIPTION_XML
app = FastAPI() app = FastAPI()
@ -266,6 +266,7 @@ async def fake_user_signin(request: Request):
return await return_edited_response(upstream_response, data_override) return await return_edited_response(upstream_response, data_override)
@app.get("/api/v2/user.json") @app.get("/api/v2/user.json")
async def fake_get_user_json(request: Request): async def fake_get_user_json(request: Request):
upstream_response = await call_official(request, request.url.path.lstrip("/")) upstream_response = await call_official(request, request.url.path.lstrip("/"))
@ -317,6 +318,42 @@ async def fake_get_user_json(request: Request):
return await return_edited_response(upstream_response, data_override) return await return_edited_response(upstream_response, data_override)
@app.post("/api/claim/exchange")
async def fake_claim_exchange(request: Request):
upstream_response = await call_official(request, request.url.path.lstrip("/"))
data_override = None
content_type = upstream_response.headers.get("content-type", "")
if content_type.startswith("application/xml"):
data = ET.fromstring(upstream_response.content)
subscription = data.find("subscription")
if subscription is not None:
parent = data
parent.remove(subscription)
parent.append(PLEXAMP_SUBSCRIPTION_XML)
# --- Replace <entitlements> ---
for ents in data.findall("entitlements"):
data.remove(ents)
entitlements = ET.SubElement(data, "entitlements", {"all": "1"})
for entitlement in ENTITLEMENTS:
ET.SubElement(entitlements, "entitlement", {"id": entitlement})
# --- Replace <roles> ---
for roles in data.findall("roles"):
data.remove(roles)
roles = ET.SubElement(data, "roles")
ET.SubElement(roles, "role", {"id": "plexpass"})
# Pretty print XML
ET.indent(data)
data_override = ET.tostring(data, encoding="utf-8", method="xml")
return await return_edited_response(upstream_response, data_override)
@app.get("/api/hack") @app.get("/api/hack")
async def hack_endpoint(): async def hack_endpoint():