From a98e602fed214c0b78defc978bb411253439e9bf Mon Sep 17 00:00:00 2001 From: ndizeyedavid Date: Sun, 9 Aug 2026 19:24:54 +0200 Subject: [PATCH 1/4] feat: add client-IP geo-bypass helpers to main.py Resolve the caller's real IP from proxy headers (X-Forwarded-For, X-Real-IP, socket peer) and forward it upstream via _geo_headers so AWS/CDN geo-locks see a residential client instead of the datacenter IP. Extend _make_request to merge those headers into every upstream call. --- main.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/main.py b/main.py index dd4dda7..ace217e 100644 --- a/main.py +++ b/main.py @@ -3,7 +3,7 @@ import json import httpx import asyncio -from fastapi import FastAPI, HTTPException, Query +from fastapi import FastAPI, HTTPException, Query, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import HTMLResponse @@ -77,12 +77,33 @@ async def _get_bearer_token() -> str: _bearer_token = m.group(1) return _bearer_token or "" -async def _make_request(url: str, method: str = "GET", payload: dict = None, custom_headers: dict = None) -> dict: +def _client_ip(request: Request | None) -> str: + """Best-effort resolve of the real client IP from proxy headers.""" + if request is None: + return "" + fwd = request.headers.get("x-forwarded-for") + if fwd: + return fwd.split(",")[0].strip() + real = request.headers.get("x-real-ip") + if real: + return real.strip() + return request.client.host if request.client else "" + + +def _geo_headers(client_ip: str) -> dict: + """Forward the caller's IP upstream so the CDN sees a residential client.""" + if not client_ip: + return {} + return {"X-Forwarded-For": client_ip, "X-Real-IP": client_ip} + + +async def _make_request(url: str, method: str = "GET", payload: dict = None, custom_headers: dict = None, client_ip: str = "") -> dict: global _bearer_token token = await _get_bearer_token() headers = { **DEFAULT_HEADERS, "Authorization": f"Bearer {token}" if token else "", + **_geo_headers(client_ip), **(custom_headers or {}) } async with httpx.AsyncClient(follow_redirects=True, timeout=25) as client: @@ -554,5 +575,5 @@ async def get_captions(subject_id: str, detail_path: str, se: int = 1, ep: int = if __name__ == "__main__": import uvicorn - port = int(os.environ.get("PORT", 8000)) + port = int(os.environ.get("PORT", 8000)) uvicorn.run("main:app", host="0.0.0.0", port=port, reload=True) From 00fcced6f2f4d80ae2315da2105e01d0528a7231 Mon Sep 17 00:00:00 2001 From: ndizeyedavid Date: Sun, 9 Aug 2026 19:25:43 +0200 Subject: [PATCH 2/4] feat: forward client IP on catalog, search and detail routes Wire the Request object into /home, /movies, /tv-series, /animation, /search, /search/suggest and /detail routes so every upstream call carries the caller's real IP via _geo_headers. This keeps the API usable from datacenter/VPS hosts behind AWS geo-locks. --- main.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/main.py b/main.py index ace217e..43a2142 100644 --- a/main.py +++ b/main.py @@ -396,9 +396,9 @@ async def dashboard(): return HTMLResponse(content=html_content) @app.get("/home") -async def get_home(): +async def get_home(request: Request): url = f"{API_BASE}/home?host=moviebox.ph" - data = await _make_request(url) + data = await _make_request(url, client_ip=_client_ip(request)) sections = [] for op in data.get("data", {}).get("operatingList", []) or []: op_type = op.get("type") @@ -424,10 +424,10 @@ async def get_home(): sections.append({"section": title, "count": len(items), "items": items}) return {"status": "success", "sections": sections} -async def _get_category_data(tab_id: int, page: int = 1, per_page: int = 24, sort: str = "RECOMMEND") -> dict: +async def _get_category_data(tab_id: int, page: int = 1, per_page: int = 24, sort: str = "RECOMMEND", client_ip: str = "") -> dict: url = f"{API_BASE}/subject/filter" payload = {"tabId": tab_id, "filter": {"sort": sort, "genre": "ALL", "country": "ALL", "year": "ALL", "language": "ALL"}, "page": page, "perPage": per_page} - data = await _make_request(url, method="POST", payload=payload) + data = await _make_request(url, method="POST", payload=payload, client_ip=client_ip) inner = data.get("data", {}) raw_items = inner.get("items", inner.get("subjects", [])) items = [{ @@ -444,21 +444,21 @@ async def _get_category_data(tab_id: int, page: int = 1, per_page: int = 24, sor return {"page": page, "per_page": per_page, "total": total, "items": items} @app.get("/movies") -async def get_movies(page: int = 1, sort: str = "RECOMMEND"): - return await _get_category_data(tab_id=2, page=page, sort=sort) +async def get_movies(request: Request, page: int = 1, sort: str = "RECOMMEND"): + return await _get_category_data(tab_id=2, page=page, sort=sort, client_ip=_client_ip(request)) @app.get("/tv-series") -async def get_tv_series(page: int = 1, sort: str = "RECOMMEND"): - return await _get_category_data(tab_id=5, page=page, sort=sort) +async def get_tv_series(request: Request, page: int = 1, sort: str = "RECOMMEND"): + return await _get_category_data(tab_id=5, page=page, sort=sort, client_ip=_client_ip(request)) @app.get("/animation") -async def get_animation(page: int = 1, sort: str = "RECOMMEND"): - return await _get_category_data(tab_id=8, page=page, sort=sort) +async def get_animation(request: Request, page: int = 1, sort: str = "RECOMMEND"): + return await _get_category_data(tab_id=8, page=page, sort=sort, client_ip=_client_ip(request)) @app.get("/search/suggest") -async def get_search_suggestions(q: str = Query(..., min_length=1)): +async def get_search_suggestions(request: Request, q: str = Query(..., min_length=1)): url = f"{API_BASE}/subject/search-suggest" - data = await _make_request(url, method="POST", payload={"keyword": q, "perPage": 10}) + data = await _make_request(url, method="POST", payload={"keyword": q, "perPage": 10}, client_ip=_client_ip(request)) inner = data.get("data", {}) raw = inner.get("items", inner.get("list", [])) suggestions = [] @@ -488,9 +488,9 @@ async def search(q: str = Query(..., min_length=1), page: int = 1): return {"query": q, "page": page, "total": total, "items": items} @app.get("/detail/{slug}") -async def get_movie_detail(slug: str): +async def get_movie_detail(request: Request, slug: str): url = f"{API_BASE}/detail?detailPath={slug}" - return await _make_request(url) + return await _make_request(url, client_ip=_client_ip(request)) @app.get("/api/stream/{subject_id}") async def get_stream_sources(subject_id: str, detail_path: str, se: int = 1, ep: int = 1): From 5b0f4f87b4b7e6ee0f5e61f3331e44f83839132d Mon Sep 17 00:00:00 2001 From: ndizeyedavid Date: Sun, 9 Aug 2026 19:26:32 +0200 Subject: [PATCH 3/4] feat: apply geo-bypass on stream and captions endpoints The play request to the netfilm.world CDN is the one AWS actually geo-locks, so forward the caller's real IP there too (X-Forwarded-For/X-Real-IP) alongside the browser-fingerprint headers, and carry it through the domain + caption lookups as well. --- main.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/main.py b/main.py index 43a2142..714bd71 100644 --- a/main.py +++ b/main.py @@ -493,9 +493,11 @@ async def get_movie_detail(request: Request, slug: str): return await _make_request(url, client_ip=_client_ip(request)) @app.get("/api/stream/{subject_id}") -async def get_stream_sources(subject_id: str, detail_path: str, se: int = 1, ep: int = 1): +async def get_stream_sources(request: Request, subject_id: str, detail_path: str, se: int = 1, ep: int = 1): + client_ip = _client_ip(request) + # Step 1: get the player domain - dom_data = await _make_request(f"{API_BASE}/media-player/get-domain") + dom_data = await _make_request(f"{API_BASE}/media-player/get-domain", client_ip=client_ip) domain = dom_data.get("data", "https://netfilm.world").rstrip("/") # Step 2: build the Referer the way the real browser player does @@ -506,7 +508,7 @@ async def get_stream_sources(subject_id: str, detail_path: str, se: int = 1, ep: play_url = f"{domain}/wefeed-h5api-bff/subject/play?subjectId={subject_id}&se={se}&ep={ep}&detailPath={detail_path}" async with httpx.AsyncClient(follow_redirects=True, timeout=25) as client: - resp = await client.get(play_url, headers={**PLAYER_HEADERS, "Referer": player_referer}) + resp = await client.get(play_url, headers={**PLAYER_HEADERS, **_geo_headers(client_ip), "Referer": player_referer}) data = resp.json().get("data", {}) has_resource = data.get("hasResource", False) @@ -535,8 +537,10 @@ async def get_stream_sources(subject_id: str, detail_path: str, se: int = 1, ep: } @app.get("/api/stream/{subject_id}/captions") -async def get_captions(subject_id: str, detail_path: str, se: int = 1, ep: int = 1): - dom_data = await _make_request(f"{API_BASE}/media-player/get-domain") +async def get_captions(request: Request, subject_id: str, detail_path: str, se: int = 1, ep: int = 1): + client_ip = _client_ip(request) + + dom_data = await _make_request(f"{API_BASE}/media-player/get-domain", client_ip=client_ip) domain = dom_data.get("data", "https://netfilm.world").rstrip("/") player_referer = ( @@ -546,7 +550,7 @@ async def get_captions(subject_id: str, detail_path: str, se: int = 1, ep: int = play_url = f"{domain}/wefeed-h5api-bff/subject/play?subjectId={subject_id}&se={se}&ep={ep}&detailPath={detail_path}" async with httpx.AsyncClient(follow_redirects=True, timeout=25) as client: - play_resp = await client.get(play_url, headers={**PLAYER_HEADERS, "Referer": player_referer}) + play_resp = await client.get(play_url, headers={**PLAYER_HEADERS, **_geo_headers(client_ip), "Referer": player_referer}) play_data = play_resp.json().get("data", {}) streams = play_data.get("streams", []) @@ -568,7 +572,7 @@ async def get_captions(subject_id: str, detail_path: str, se: int = 1, ep: int = f"{API_BASE}/subject/caption" f"?format={stream_format}&id={stream_id}&subjectId={subject_id}&detailPath={detail_path}" ) - data = await _make_request(cap_url) + data = await _make_request(cap_url, client_ip=client_ip) inner = data.get("data", {}) captions = inner.get("captions", []) if isinstance(inner, dict) else inner return {"subject_id": subject_id, "se": se, "ep": ep, "count": len(captions), "captions": captions} From 4e404de797621c38904454123bfddbfdc2fd5f4a Mon Sep 17 00:00:00 2001 From: ndizeyedavid Date: Sun, 9 Aug 2026 19:28:12 +0200 Subject: [PATCH 4/4] feat: mirror geo-lock bypass into api.py The v2.2.0 aoneroom direct-stream server gets the same treatment: _client_ip/_geo_headers helpers, client-IP propagation through every route, and the real IP forwarded on the stream/caption play requests so the AWS geo-lock no longer blocks VPS deployments. --- api.py | 71 +++++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 48 insertions(+), 23 deletions(-) diff --git a/api.py b/api.py index 5fddd0e..a237140 100644 --- a/api.py +++ b/api.py @@ -2,7 +2,7 @@ import json import httpx import asyncio -from fastapi import FastAPI, HTTPException, Query +from fastapi import FastAPI, HTTPException, Query, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import HTMLResponse @@ -73,12 +73,33 @@ async def _get_bearer_token() -> str: _bearer_token = m.group(1) return _bearer_token or "" -async def _make_request(url: str, method: str = "GET", payload: dict = None, custom_headers: dict = None) -> dict: +def _client_ip(request: Request | None) -> str: + """Best-effort resolve of the real client IP from proxy headers.""" + if request is None: + return "" + fwd = request.headers.get("x-forwarded-for") + if fwd: + return fwd.split(",")[0].strip() + real = request.headers.get("x-real-ip") + if real: + return real.strip() + return request.client.host if request.client else "" + + +def _geo_headers(client_ip: str) -> dict: + """Forward the caller's IP upstream so the CDN sees a residential client.""" + if not client_ip: + return {} + return {"X-Forwarded-For": client_ip, "X-Real-IP": client_ip} + + +async def _make_request(url: str, method: str = "GET", payload: dict = None, custom_headers: dict = None, client_ip: str = "") -> dict: global _bearer_token token = await _get_bearer_token() headers = { **DEFAULT_HEADERS, "Authorization": f"Bearer {token}" if token else "", + **_geo_headers(client_ip), **(custom_headers or {}) } async with httpx.AsyncClient(follow_redirects=True, timeout=25) as client: @@ -281,9 +302,9 @@ async def dashboard(): return HTMLResponse(content=html_content) @app.get("/home") -async def get_home(): +async def get_home(request: Request): url = f"{API_BASE}/home?host=moviebox.ph" - data = await _make_request(url) + data = await _make_request(url, client_ip=_client_ip(request)) sections = [] for op in data.get("data", {}).get("operatingList", []) or []: op_type = op.get("type") @@ -309,10 +330,10 @@ async def get_home(): sections.append({"section": title, "count": len(items), "items": items}) return {"status": "success", "sections": sections} -async def _get_category_data(tab_id: int, page: int = 1, per_page: int = 24, sort: str = "RECOMMEND") -> dict: +async def _get_category_data(tab_id: int, page: int = 1, per_page: int = 24, sort: str = "RECOMMEND", client_ip: str = "") -> dict: url = f"{API_BASE}/subject/filter" payload = {"tabId": tab_id, "filter": {"sort": sort, "genre": "ALL", "country": "ALL", "year": "ALL", "language": "ALL"}, "page": page, "perPage": per_page} - data = await _make_request(url, method="POST", payload=payload) + data = await _make_request(url, method="POST", payload=payload, client_ip=client_ip) inner = data.get("data", {}) raw_items = inner.get("items", inner.get("subjects", [])) items = [{ @@ -329,21 +350,21 @@ async def _get_category_data(tab_id: int, page: int = 1, per_page: int = 24, sor return {"page": page, "per_page": per_page, "total": total, "items": items} @app.get("/movies") -async def get_movies(page: int = 1, sort: str = "RECOMMEND"): - return await _get_category_data(tab_id=2, page=page, sort=sort) +async def get_movies(request: Request, page: int = 1, sort: str = "RECOMMEND"): + return await _get_category_data(tab_id=2, page=page, sort=sort, client_ip=_client_ip(request)) @app.get("/tv-series") -async def get_tv_series(page: int = 1, sort: str = "RECOMMEND"): - return await _get_category_data(tab_id=5, page=page, sort=sort) +async def get_tv_series(request: Request, page: int = 1, sort: str = "RECOMMEND"): + return await _get_category_data(tab_id=5, page=page, sort=sort, client_ip=_client_ip(request)) @app.get("/animation") -async def get_animation(page: int = 1, sort: str = "RECOMMEND"): - return await _get_category_data(tab_id=8, page=page, sort=sort) +async def get_animation(request: Request, page: int = 1, sort: str = "RECOMMEND"): + return await _get_category_data(tab_id=8, page=page, sort=sort, client_ip=_client_ip(request)) @app.get("/search/suggest") -async def get_search_suggestions(q: str = Query(..., min_length=1)): +async def get_search_suggestions(request: Request, q: str = Query(..., min_length=1)): url = f"{API_BASE}/subject/search-suggest" - data = await _make_request(url, method="POST", payload={"keyword": q, "perPage": 10}) + data = await _make_request(url, method="POST", payload={"keyword": q, "perPage": 10}, client_ip=_client_ip(request)) inner = data.get("data", {}) raw = inner.get("items", inner.get("list", [])) suggestions = [] @@ -357,9 +378,9 @@ async def get_search_suggestions(q: str = Query(..., min_length=1)): return {"suggestions": suggestions} @app.get("/search") -async def search(q: str = Query(..., min_length=1), page: int = 1): +async def search(request: Request, q: str = Query(..., min_length=1), page: int = 1): url = f"{API_BASE}/subject/search" - data = await _make_request(url, method="POST", payload={"keyword": q, "page": page, "perPage": 20}) + data = await _make_request(url, method="POST", payload={"keyword": q, "page": page, "perPage": 20}, client_ip=_client_ip(request)) inner = data.get("data", {}) raw = inner.get("items", inner.get("list", [])) items = [{ @@ -373,22 +394,24 @@ async def search(q: str = Query(..., min_length=1), page: int = 1): return {"query": q, "page": page, "total": total, "items": items} @app.get("/detail/{slug}") -async def get_movie_detail(slug: str): +async def get_movie_detail(request: Request, slug: str): url = f"{API_BASE}/detail?detailPath={slug}" - return await _make_request(url) + return await _make_request(url, client_ip=_client_ip(request)) # ---------------------------------------------------- # 📌 āĻĢāĻŋāĻ•ā§āϏ āĻ•āϰāĻž āĻ¸ā§āĻŸā§āϰāĻŋāĻŽāĻŋāĻ‚ āĻāĻ¨ā§āĻĄāĻĒāϝāĻŧ⧇āĻ¨ā§āϟ (Aoneroom Direct MP4 Stream) # ---------------------------------------------------- @app.get("/api/stream/{subject_id}") -async def get_stream_sources(subject_id: str, detail_path: str = "", se: int = 0, ep: int = 0): +async def get_stream_sources(request: Request, subject_id: str, detail_path: str = "", se: int = 0, ep: int = 0): + client_ip = _client_ip(request) + # āφāĻĒāύāĻžāϰ āϰāĻŋāĻ•āĻŽā§‡āĻ¨ā§āĻĄ āĻ•āϰāĻž āĻ“ā§ŸāĻžāĻ°ā§āĻ•āĻŋāĻ‚ āĻĒāĻžāĻĨ play_url = f"{STREAM_BASE}/web/subject/play?subjectId={subject_id}&se={se}&ep={ep}&detailPath={detail_path}" player_referer = f"https://h5.aoneroom.com/spa/videoPlayPage/movies/{detail_path}?id={subject_id}&type=/movie/detail&detailSe={se}&detailEp={ep}&lang=en" async with httpx.AsyncClient(follow_redirects=True, timeout=25) as client: - resp = await client.get(play_url, headers={**PLAYER_HEADERS, "Referer": player_referer}) + resp = await client.get(play_url, headers={**PLAYER_HEADERS, **_geo_headers(client_ip), "Referer": player_referer}) if resp.status_code != 200: raise HTTPException(status_code=502, detail="Stream service unavailable") @@ -424,12 +447,14 @@ async def get_stream_sources(subject_id: str, detail_path: str = "", se: int = 0 } @app.get("/api/stream/{subject_id}/captions") -async def get_captions(subject_id: str, detail_path: str = "", se: int = 0, ep: int = 0): +async def get_captions(request: Request, subject_id: str, detail_path: str = "", se: int = 0, ep: int = 0): + client_ip = _client_ip(request) + play_url = f"{STREAM_BASE}/web/subject/play?subjectId={subject_id}&se={se}&ep={ep}&detailPath={detail_path}" player_referer = f"https://h5.aoneroom.com/spa/videoPlayPage/movies/{detail_path}?id={subject_id}&type=/movie/detail&detailSe={se}&detailEp={ep}&lang=en" async with httpx.AsyncClient(follow_redirects=True, timeout=25) as client: - play_resp = await client.get(play_url, headers={**PLAYER_HEADERS, "Referer": player_referer}) + play_resp = await client.get(play_url, headers={**PLAYER_HEADERS, **_geo_headers(client_ip), "Referer": player_referer}) play_data = play_resp.json().get("data", {}) streams = play_data.get("streams", []) @@ -451,7 +476,7 @@ async def get_captions(subject_id: str, detail_path: str = "", se: int = 0, ep: f"{API_BASE}/subject/caption" f"?format={stream_format}&id={stream_id}&subjectId={subject_id}&detailPath={detail_path}" ) - data = await _make_request(cap_url) + data = await _make_request(cap_url, client_ip=client_ip) inner = data.get("data", {}) captions = inner.get("captions", []) if isinstance(inner, dict) else inner return {"subject_id": subject_id, "se": se, "ep": ep, "count": len(captions), "captions": captions}