diff --git a/api/main.py b/api/main.py index c750d17..b5dbd5b 100644 --- a/api/main.py +++ b/api/main.py @@ -14,25 +14,13 @@ from fastapi import ( HTTPException, ) from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import RedirectResponse, FileResponse, StreamingResponse +from fastapi.responses import FileResponse, StreamingResponse from fastapi.templating import Jinja2Templates from api import middleware from db import database, crud, schemas from db.config import settings -from streaming_providers.alldebrid.utils import get_direct_link_from_alldebrid -from streaming_providers.debridlink.api import router as debridlink_router -from streaming_providers.debridlink.utils import get_direct_link_from_debridlink -from streaming_providers.exceptions import ProviderException -from streaming_providers.offcloud.utils import get_direct_link_from_offcloud -from streaming_providers.pikpak.utils import get_direct_link_from_pikpak -from streaming_providers.premiumize.api import router as premiumize_router -from streaming_providers.premiumize.utils import get_direct_link_from_premiumize -from streaming_providers.realdebrid.api import router as realdebrid_router -from streaming_providers.realdebrid.utils import get_direct_link_from_realdebrid -from streaming_providers.seedr.api import router as seedr_router -from streaming_providers.seedr.utils import get_direct_link_from_seedr -from streaming_providers.torbox.utils import get_direct_link_from_torbox +from streaming_providers.routes import router as streaming_provider_router from utils import crypto, torrent, poster, const, rate_limiter from utils.parser import generate_manifest @@ -58,16 +46,6 @@ app.add_middleware(middleware.SecureLoggingMiddleware) app.add_middleware(middleware.UserDataMiddleware) TEMPLATES = Jinja2Templates(directory="resources") -headers = { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Headers": "*", - "Cache-Control": "max-age=3600, stale-while-revalidate=3600, stale-if-error=604800, public", -} -no_cache_headers = { - "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0", - "Pragma": "no-cache", - "Expires": "0", -} def get_user_data(request: Request) -> schemas.UserData: @@ -143,7 +121,7 @@ async def get_favicon(): @app.get("/static/{file_path:path}") async def function(file_path: str): response = FileResponse(f"resources/{file_path}") - response.headers.update(headers) + response.headers.update(const.DEFAULT_HEADERS) return response @@ -154,8 +132,7 @@ async def configure( request: Request, user_data: schemas.UserData = Depends(get_user_data), ): - response.headers.update(headers) - response.headers.update(no_cache_headers) + response.headers.update(const.NO_CACHE_HEADERS) # Remove the password from the streaming provider if user_data.streaming_provider: @@ -177,7 +154,7 @@ async def configure( async def get_manifest( response: Response, user_data: schemas.UserData = Depends(get_user_data) ): - response.headers.update({**headers, **no_cache_headers}) + response.headers.update(const.NO_CACHE_HEADERS) with open("resources/manifest.json") as file: manifest = json.load(file) @@ -236,7 +213,7 @@ async def get_catalog( genre: str = None, user_data: schemas.UserData = Depends(get_user_data), ): - response.headers.update(headers) + response.headers.update(const.DEFAULT_HEADERS) if genre and "&" in genre: genre, skip = genre.split("&") skip = skip.split("=")[1] if "=" in skip else "0" @@ -246,7 +223,7 @@ async def get_catalog( if user_data.streaming_provider and catalog_id.startswith( user_data.streaming_provider.service ): - response.headers.update(no_cache_headers) + response.headers.update(const.NO_CACHE_HEADERS) cache_key = None # Try retrieving the cached data @@ -295,7 +272,7 @@ async def search_meta( ], search_query: str, ): - response.headers.update(headers) + response.headers.update(const.DEFAULT_HEADERS) logging.debug("search for catalog_id: %s", catalog_id) return await crud.process_search_query(search_query, catalog_type) @@ -321,7 +298,7 @@ async def get_meta( response: Response, request: Request, ): - response.headers.update(headers) + response.headers.update(const.DEFAULT_HEADERS) cache_key = f"{catalog_type}_{meta_id}_meta" # Try retrieving the cached data @@ -380,7 +357,7 @@ async def get_streams( episode: int = None, user_data: schemas.UserData = Depends(get_user_data), ): - response.headers.update(headers) + response.headers.update(const.DEFAULT_HEADERS) if catalog_type == "movie": fetched_streams = await crud.get_movie_streams( @@ -402,82 +379,6 @@ async def encrypt_user_data(user_data: schemas.UserData): return {"encrypted_str": encrypted_str} -@app.get("/{secret_str}/streaming_provider", tags=["streaming_provider"]) -@rate_limiter.exclude -async def streaming_provider_endpoint( - secret_str: str, - info_hash: str, - response: Response, - request: Request, - season: int = None, - episode: int = None, -): - response.headers.update(headers) - response.headers.update(no_cache_headers) - - user_data = request.scope.get("user", crypto.decrypt_user_data(secret_str)) - if not user_data.streaming_provider: - raise HTTPException(status_code=400, detail="No streaming provider set.") - - stream = await crud.get_stream_by_info_hash(info_hash) - if not stream: - raise HTTPException(status_code=400, detail="Stream not found.") - - magnet_link = await torrent.convert_info_hash_to_magnet( - info_hash, stream.announce_list - ) - - episode_data = stream.get_episode(season, episode) - filename = episode_data.filename if episode_data else stream.filename - - try: - if user_data.streaming_provider.service == "seedr": - video_url = await get_direct_link_from_seedr( - info_hash, magnet_link, user_data, stream, filename, 1, 0 - ) - elif user_data.streaming_provider.service == "realdebrid": - video_url = get_direct_link_from_realdebrid( - info_hash, magnet_link, user_data, filename, stream.file_index, 1, 0 - ) - elif user_data.streaming_provider.service == "alldebrid": - video_url = get_direct_link_from_alldebrid( - info_hash, magnet_link, user_data, filename, 1, 0 - ) - elif user_data.streaming_provider.service == "offcloud": - video_url = get_direct_link_from_offcloud( - info_hash, magnet_link, user_data, filename, 1, 0 - ) - elif user_data.streaming_provider.service == "pikpak": - video_url = await get_direct_link_from_pikpak( - info_hash, magnet_link, user_data, stream, filename, 1, 0 - ) - elif user_data.streaming_provider.service == "torbox": - video_url = get_direct_link_from_torbox( - info_hash, magnet_link, user_data, filename, 1, 0 - ) - elif user_data.streaming_provider.service == "premiumize": - video_url = get_direct_link_from_premiumize( - info_hash, magnet_link, user_data, stream.torrent_name, filename, 1, 0 - ) - else: - video_url = get_direct_link_from_debridlink( - info_hash, magnet_link, user_data, filename, stream.file_index, 1, 0 - ) - except ProviderException as error: - logging.error( - "Exception occurred for %s: %s", - info_hash, - error.message, - exc_info=True if error.video_file_name == "api_error.mp4" else False, - ) - video_url = f"{settings.host_url}/static/exceptions/{error.video_file_name}" - except Exception as e: - logging.error("Exception occurred for %s: %s", info_hash, e, exc_info=True) - video_url = f"{settings.host_url}/static/exceptions/api_error.mp4" - - return RedirectResponse(url=video_url, headers=response.headers) - - @app.get("/poster/{catalog_type}/{mediafusion_id}.jpg", tags=["poster"]) @rate_limiter.exclude async def get_poster( @@ -516,7 +417,7 @@ async def get_poster( image_byte_io.seek(0) return StreamingResponse( - image_byte_io, media_type="image/jpeg", headers=headers + image_byte_io, media_type="image/jpeg", headers=const.DEFAULT_HEADERS ) except Exception as e: logging.error(f"Unexpected error while creating poster: {e}") @@ -525,7 +426,6 @@ async def get_poster( raise HTTPException(status_code=404, detail="Failed to create poster.") -app.include_router(seedr_router, prefix="/seedr", tags=["seedr"]) -app.include_router(realdebrid_router, prefix="/realdebrid", tags=["realdebrid"]) -app.include_router(debridlink_router, prefix="/debridlink", tags=["debridlink"]) -app.include_router(premiumize_router, prefix="/premiumize", tags=["premiumize"]) +app.include_router( + streaming_provider_router, prefix="/streaming_provider", tags=["streaming_provider"] +) diff --git a/api/middleware.py b/api/middleware.py index 25f404d..31816de 100644 --- a/api/middleware.py +++ b/api/middleware.py @@ -10,7 +10,7 @@ from starlette.routing import Match from db.config import settings from db.schemas import UserData -from utils import crypto +from utils import crypto, const def get_client_ip(request: Request) -> str | None: @@ -107,10 +107,7 @@ class RateLimitMiddleware(BaseHTTPMiddleware): return Response( content="Rate limit exceeded", status_code=429, - headers={ - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Headers": "*", - }, + headers=const.NO_CACHE_HEADERS, ) return await call_next(request) diff --git a/resources/js/config_script.js b/resources/js/config_script.js index 22f8a1f..4ad6162 100644 --- a/resources/js/config_script.js +++ b/resources/js/config_script.js @@ -206,13 +206,13 @@ oAuthBtn.addEventListener('click', async function () { document.getElementById('provider_token').disabled = true; if (provider === 'seedr') { - await initiateOAuthFlow('/seedr/get-device-code', '/seedr/authorize'); + await initiateOAuthFlow('/streaming_provider/seedr/get-device-code', '/streaming_provider/seedr/authorize'); } else if (provider === 'realdebrid') { - await initiateOAuthFlow('/realdebrid/get-device-code', '/realdebrid/authorize'); + await initiateOAuthFlow('/streaming_provider/realdebrid/get-device-code', '/streaming_provider/realdebrid/authorize'); } else if (provider === 'debridlink') { - await initiateOAuthFlow('/debridlink/get-device-code', '/debridlink/authorize') + await initiateOAuthFlow('/streaming_provider/debridlink/get-device-code', '/streaming_provider/debridlink/authorize') } else if (provider === 'premiumize') { - return window.location.href = "/premiumize/authorize"; + return window.location.href = "/streaming_provider/premiumize/authorize"; } }); diff --git a/streaming_providers/debridlink/api.py b/streaming_providers/debridlink/api.py index 2b877f3..deb0d2b 100644 --- a/streaming_providers/debridlink/api.py +++ b/streaming_providers/debridlink/api.py @@ -3,19 +3,21 @@ from fastapi.responses import JSONResponse from db.schemas import AuthorizeData from streaming_providers.debridlink.client import DebridLink +from utils import const router = APIRouter() -headers = {"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0", "Pragma": "no-cache", "Expires": "0"} @router.get("/get-device-code") async def get_device_code(): dl_client = DebridLink() - return JSONResponse(content=dl_client.get_device_code(), headers=headers) + return JSONResponse( + content=dl_client.get_device_code(), headers=const.NO_CACHE_HEADERS + ) @router.post("/authorize") async def authorize(data: AuthorizeData): dl_client = DebridLink() response = dl_client.authorize(data.device_code) - return JSONResponse(content=response, headers=headers) + return JSONResponse(content=response, headers=const.NO_CACHE_HEADERS) diff --git a/streaming_providers/premiumize/api.py b/streaming_providers/premiumize/api.py index 1edca85..b801f31 100644 --- a/streaming_providers/premiumize/api.py +++ b/streaming_providers/premiumize/api.py @@ -3,14 +3,9 @@ from fastapi.responses import RedirectResponse from db import schemas from streaming_providers.premiumize.client import Premiumize -from utils import crypto +from utils import crypto, const router = APIRouter() -headers = { - "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0", - "Pragma": "no-cache", - "Expires": "0", -} @router.get("/authorize") @@ -19,7 +14,9 @@ async def authorize(): return {"error": "Premiumize OAuth not configured"} premiumize_client = Premiumize() - return RedirectResponse(premiumize_client.get_authorization_url(), headers=headers) + return RedirectResponse( + premiumize_client.get_authorization_url(), headers=const.NO_CACHE_HEADERS + ) @router.get("/oauth2_redirect") @@ -31,4 +28,6 @@ async def oauth2_redirect(code: str): streaming_provider=schemas.StreamingProvider(service="premiumize", token=token) ) encrypted_str = crypto.encrypt_user_data(user_data) - return RedirectResponse(f"/{encrypted_str}/configure", headers=headers) + return RedirectResponse( + f"/{encrypted_str}/configure", headers=const.NO_CACHE_HEADERS + ) diff --git a/streaming_providers/realdebrid/api.py b/streaming_providers/realdebrid/api.py index d9e95d7..c4e2953 100644 --- a/streaming_providers/realdebrid/api.py +++ b/streaming_providers/realdebrid/api.py @@ -3,19 +3,21 @@ from fastapi.responses import JSONResponse from db.schemas import AuthorizeData from streaming_providers.realdebrid.client import RealDebrid +from utils import const router = APIRouter() -headers = {"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0", "Pragma": "no-cache", "Expires": "0"} @router.get("/get-device-code") async def get_device_code(): rd_client = RealDebrid() - return JSONResponse(content=rd_client.get_device_code(), headers=headers) + return JSONResponse( + content=rd_client.get_device_code(), headers=const.NO_CACHE_HEADERS + ) @router.post("/authorize") async def authorize(data: AuthorizeData): rd_client = RealDebrid() response = rd_client.authorize(data.device_code) - return JSONResponse(content=response, headers=headers) + return JSONResponse(content=response, headers=const.NO_CACHE_HEADERS) diff --git a/streaming_providers/routes.py b/streaming_providers/routes.py new file mode 100644 index 0000000..7b0bf01 --- /dev/null +++ b/streaming_providers/routes.py @@ -0,0 +1,109 @@ +import logging + +from fastapi import ( + Request, + Response, + HTTPException, + APIRouter, +) +from fastapi.responses import RedirectResponse + +from db import crud +from db.config import settings +from streaming_providers.alldebrid.utils import get_direct_link_from_alldebrid +from streaming_providers.debridlink.api import router as debridlink_router +from streaming_providers.debridlink.utils import get_direct_link_from_debridlink +from streaming_providers.exceptions import ProviderException +from streaming_providers.offcloud.utils import get_direct_link_from_offcloud +from streaming_providers.pikpak.utils import get_direct_link_from_pikpak +from streaming_providers.premiumize.api import router as premiumize_router +from streaming_providers.premiumize.utils import get_direct_link_from_premiumize +from streaming_providers.realdebrid.api import router as realdebrid_router +from streaming_providers.realdebrid.utils import get_direct_link_from_realdebrid +from streaming_providers.seedr.api import router as seedr_router +from streaming_providers.seedr.utils import get_direct_link_from_seedr +from streaming_providers.torbox.utils import get_direct_link_from_torbox +from utils import crypto, torrent, rate_limiter, const + +router = APIRouter() + + +@router.get("/{secret_str}/stream", tags=["streaming_provider"]) +@rate_limiter.exclude +async def streaming_provider_endpoint( + secret_str: str, + info_hash: str, + response: Response, + request: Request, + season: int = None, + episode: int = None, +): + response.headers.update(const.NO_CACHE_HEADERS) + + user_data = request.scope.get("user", crypto.decrypt_user_data(secret_str)) + if not user_data.streaming_provider: + raise HTTPException(status_code=400, detail="No streaming provider set.") + + stream = await crud.get_stream_by_info_hash(info_hash) + if not stream: + raise HTTPException(status_code=400, detail="Stream not found.") + + magnet_link = await torrent.convert_info_hash_to_magnet( + info_hash, stream.announce_list + ) + + episode_data = stream.get_episode(season, episode) + filename = episode_data.filename if episode_data else stream.filename + + try: + if user_data.streaming_provider.service == "seedr": + video_url = await get_direct_link_from_seedr( + info_hash, magnet_link, user_data, stream, filename, 1, 0 + ) + elif user_data.streaming_provider.service == "realdebrid": + video_url = get_direct_link_from_realdebrid( + info_hash, magnet_link, user_data, filename, stream.file_index, 1, 0 + ) + elif user_data.streaming_provider.service == "alldebrid": + video_url = get_direct_link_from_alldebrid( + info_hash, magnet_link, user_data, filename, 1, 0 + ) + elif user_data.streaming_provider.service == "offcloud": + video_url = get_direct_link_from_offcloud( + info_hash, magnet_link, user_data, filename, 1, 0 + ) + elif user_data.streaming_provider.service == "pikpak": + video_url = await get_direct_link_from_pikpak( + info_hash, magnet_link, user_data, stream, filename, 1, 0 + ) + elif user_data.streaming_provider.service == "torbox": + video_url = get_direct_link_from_torbox( + info_hash, magnet_link, user_data, filename, 1, 0 + ) + elif user_data.streaming_provider.service == "premiumize": + video_url = get_direct_link_from_premiumize( + info_hash, magnet_link, user_data, stream.torrent_name, filename, 1, 0 + ) + else: + video_url = get_direct_link_from_debridlink( + info_hash, magnet_link, user_data, filename, stream.file_index, 1, 0 + ) + except ProviderException as error: + logging.error( + "Exception occurred for %s: %s", + info_hash, + error.message, + exc_info=True if error.video_file_name == "api_error.mp4" else False, + ) + video_url = f"{settings.host_url}/static/exceptions/{error.video_file_name}" + except Exception as e: + logging.error("Exception occurred for %s: %s", info_hash, e, exc_info=True) + video_url = f"{settings.host_url}/static/exceptions/api_error.mp4" + + return RedirectResponse(url=video_url, headers=response.headers) + + +router.include_router(seedr_router, prefix="/seedr", tags=["seedr"]) +router.include_router(realdebrid_router, prefix="/realdebrid", tags=["realdebrid"]) +router.include_router(debridlink_router, prefix="/debridlink", tags=["debridlink"]) +router.include_router(premiumize_router, prefix="/premiumize", tags=["premiumize"]) diff --git a/streaming_providers/seedr/api.py b/streaming_providers/seedr/api.py index 416e67d..ae440af 100644 --- a/streaming_providers/seedr/api.py +++ b/streaming_providers/seedr/api.py @@ -3,20 +3,16 @@ from fastapi.responses import JSONResponse from seedrcc import Login from db.schemas import AuthorizeData +from utils import const router = APIRouter() -headers = { - "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0", - "Pragma": "no-cache", - "Expires": "0", -} @router.get("/get-device-code") async def get_device_code(): seedr = Login() device_code = seedr.getDeviceCode() - return JSONResponse(content=device_code, headers=headers) + return JSONResponse(content=device_code, headers=const.NO_CACHE_HEADERS) @router.post("/authorize") @@ -25,6 +21,8 @@ async def authorize(data: AuthorizeData): response = seedr.authorize(data.device_code) if "access_token" in response: - return JSONResponse(content={"token": seedr.token}, headers=headers) + return JSONResponse( + content={"token": seedr.token}, headers=const.NO_CACHE_HEADERS + ) else: - return JSONResponse(content=response, headers=headers) + return JSONResponse(content=response, headers=const.NO_CACHE_HEADERS) diff --git a/utils/const.py b/utils/const.py index 2aec8be..236ee30 100644 --- a/utils/const.py +++ b/utils/const.py @@ -86,3 +86,17 @@ RESOLUTIONS = [ DEBRID_SERVER_TIMEOUT = 15 + + +DEFAULT_HEADERS = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "*", + "Cache-Control": "max-age=3600, stale-while-revalidate=3600, stale-if-error=604800, public", +} +NO_CACHE_HEADERS = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "*", + "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0", + "Pragma": "no-cache", + "Expires": "0", +} diff --git a/utils/parser.py b/utils/parser.py index ffedd21..56f7f8d 100644 --- a/utils/parser.py +++ b/utils/parser.py @@ -152,7 +152,7 @@ async def parse_stream_data( } if user_data.streaming_provider: - base_proxy_url = f"{settings.host_url}/{secret_str}/streaming_provider?info_hash={stream_data.id}" + base_proxy_url = f"{settings.host_url}/streaming_provider/{secret_str}/stream?info_hash={stream_data.id}" if episode_data: base_proxy_url += f"&season={season}&episode={episode}" stream_details["url"] = base_proxy_url