diff --git a/.env-sample b/.env-sample index d257864..af7c718 100644 --- a/.env-sample +++ b/.env-sample @@ -13,4 +13,7 @@ INDEXER_MANAGER_TIMEOUT=60 # maximum time to obtain search results from indexer INDEXER_MANAGER_INDEXERS='["EXAMPLE1_CHANGETHIS", "EXAMPLE2_CHANGETHIS"]' GET_TORRENT_TIMEOUT=5 # maximum time to obtain the torrent info hash in seconds ZILEAN_URL=None # for DMM search - https://github.com/iPromKnight/zilean -CUSTOM_HEADER_HTML=None # only set it if you know what it is \ No newline at end of file +CUSTOM_HEADER_HTML=None # only set it if you know what it is +PROXY_DEBRID_STREAM=False # Proxy Debrid Streams (very useful to use your debrid service on multiple IPs at same time) +PROXY_DEBRID_STREAM_PASSWORD=CHANGE_ME # Secret password to enter on configuration page to prevent people from abusing your debrid stream proxy +PROXY_DEBRID_STREAM_BYTES_PER_CHUNK=102400 # 10MB per chunks \ No newline at end of file diff --git a/comet/api/stream.py b/comet/api/stream.py index 6318188..3654ed8 100644 --- a/comet/api/stream.py +++ b/comet/api/stream.py @@ -5,7 +5,7 @@ import time import aiohttp from fastapi import APIRouter, Request -from fastapi.responses import RedirectResponse +from fastapi.responses import RedirectResponse, StreamingResponse from RTN import Torrent, parse, sort_torrents, title_match from comet.debrid.manager import getDebrid @@ -279,7 +279,7 @@ async def playback(b64config: str, hash: str, index: str): @streams.get("/{b64config}/playback/{hash}/{index}") -async def playback(b64config: str, hash: str, index: str): +async def playback(request: Request, b64config: str, hash: str, index: str): config = config_check(b64config) if not config: return @@ -288,4 +288,46 @@ async def playback(b64config: str, hash: str, index: str): debrid = getDebrid(session, config) download_link = await debrid.generate_download_link(hash, index) - return RedirectResponse(download_link, status_code=302) + if ( + settings.PROXY_DEBRID_STREAM + and settings.PROXY_DEBRID_STREAM_PASSWORD + == config["debridStreamProxyPassword"] + ): + + async def stream_content(headers: dict): + async with aiohttp.ClientSession() as session: + response = await session.get(download_link, headers=headers) + while True: + chunk = await response.content.read( + settings.PROXY_DEBRID_STREAM_BYTES_PER_CHUNK + ) # 10 MB chunks + if not chunk: + break + yield chunk + + range = None + range_header = request.headers.get("range") + if range_header: + range_value = range_header.strip().split("=")[1] + start, end = range_value.split("-") + start = int(start) + end = int(end) if end else "" + range = f"bytes={start}-{end}" + + response = await session.get( + download_link, headers={"Range": f"bytes={start}-{end}"} + ) + if response.status == 206: + return StreamingResponse( + stream_content({"Range": range}), + status_code=206, + headers={ + "Content-Range": response.headers["Content-Range"], + "Content-Length": response.headers["Content-Length"], + "Accept-Ranges": "bytes", + }, + ) + + return + + return RedirectResponse(download_link, status_code=302) diff --git a/comet/debrid/manager.py b/comet/debrid/manager.py index aa30333..cd1cc4a 100644 --- a/comet/debrid/manager.py +++ b/comet/debrid/manager.py @@ -7,4 +7,4 @@ def getDebrid(session: aiohttp.ClientSession, config: dict): debrid_service = config["debridService"] debrid_api_key = config["debridApiKey"] if debrid_service == "realdebrid": - return RealDebrid(session, debrid_api_key) \ No newline at end of file + return RealDebrid(session, debrid_api_key) diff --git a/comet/debrid/realdebrid.py b/comet/debrid/realdebrid.py index 2e43b6e..a79b4da 100644 --- a/comet/debrid/realdebrid.py +++ b/comet/debrid/realdebrid.py @@ -16,9 +16,7 @@ class RealDebrid: async def check_premium(self): try: - check_premium = await self.session.get( - f"{self.api_url}/user" - ) + check_premium = await self.session.get(f"{self.api_url}/user") check_premium = await check_premium.text() if '"type": "premium"' not in check_premium: return False @@ -29,7 +27,7 @@ class RealDebrid: f"Exception while checking premium status on Real Debrid: {e}" ) return False - + async def get_instant(self, hash: str): try: response = await self.session.get( @@ -58,7 +56,6 @@ class RealDebrid: return availability - async def get_files(self, availability: dict, type: str, season: str, episode: str): files = {} for hash, details in availability.items(): diff --git a/comet/templates/index.html b/comet/templates/index.html index 78c88a9..32c3434 100644 --- a/comet/templates/index.html +++ b/comet/templates/index.html @@ -514,6 +514,10 @@ Filter Titles +
+ +
+
Real-Debrid @@ -578,18 +582,20 @@ const filterTitles = document.getElementById("filterTitles").checked; const debridService = document.getElementById("debridService").value; const debridApiKey = document.getElementById("debridApiKey").value; + const debridStreamProxyPassword = document.getElementById("debridStreamProxyPassword").value; const selectedLanguages = languages.length === defaultLanguages.length && languages.every((val, index) => val === defaultLanguages[index]) ? ["All"] : languages; const selectedResolutions = resolutions.length === defaultResolutions.length && resolutions.every((val, index) => val === defaultResolutions[index]) ? ["All"] : resolutions; const settings = { - debridService: debridService, - debridApiKey: debridApiKey, indexers: indexers, maxResults: parseInt(maxResults), filterTitles: filterTitles, resolutions: selectedResolutions, - languages: selectedLanguages + languages: selectedLanguages, + debridService: debridService, + debridApiKey: debridApiKey, + debridStreamProxyPassword: debridStreamProxyPassword, }; navigator.clipboard.writeText(`${window.location.origin}/${btoa(JSON.stringify(settings))}/manifest.json`).then(() => { diff --git a/comet/utils/models.py b/comet/utils/models.py index cf8b9ce..f99e43b 100644 --- a/comet/utils/models.py +++ b/comet/utils/models.py @@ -10,22 +10,25 @@ from RTN import RTN, BaseRankingModel, SettingsModel class AppSettings(BaseSettings): model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8") - ADDON_ID: str = "stremio.comet.fast" - ADDON_NAME: str = "Comet" - FASTAPI_HOST: str = "0.0.0.0" - FASTAPI_PORT: int = 8000 - FASTAPI_WORKERS: int = 2 * (os.cpu_count() or 1) - DATABASE_PATH: str = "data/comet.db" - CACHE_TTL: int = 86400 + ADDON_ID: Optional[str] = "stremio.comet.fast" + ADDON_NAME: Optional[str] = "Comet" + FASTAPI_HOST: Optional[str] = "0.0.0.0" + FASTAPI_PORT: Optional[int] = 8000 + FASTAPI_WORKERS: Optional[int] = 2 * (os.cpu_count() or 1) + DATABASE_PATH: Optional[str] = "data/comet.db" + CACHE_TTL: Optional[int] = 86400 DEBRID_PROXY_URL: Optional[str] = None INDEXER_MANAGER_TYPE: str = "jackett" INDEXER_MANAGER_URL: str = "http://127.0.0.1:9117" - INDEXER_MANAGER_API_KEY: str = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" - INDEXER_MANAGER_TIMEOUT: int = 30 + INDEXER_MANAGER_API_KEY: str + INDEXER_MANAGER_TIMEOUT: Optional[int] = 30 INDEXER_MANAGER_INDEXERS: List[str] = ["EXAMPLE1_CHANGETHIS", "EXAMPLE2_CHANGETHIS"] - GET_TORRENT_TIMEOUT: int = 5 + GET_TORRENT_TIMEOUT: Optional[int] = 5 ZILEAN_URL: Optional[str] = None CUSTOM_HEADER_HTML: Optional[str] = None + PROXY_DEBRID_STREAM: Optional[bool] = False + PROXY_DEBRID_STREAM_PASSWORD: Optional[str] = "CHANGE_ME" + PROXY_DEBRID_STREAM_BYTES_PER_CHUNK: Optional[int] = 102400 settings = AppSettings() @@ -39,6 +42,7 @@ class ConfigModel(BaseModel): filterTitles: Optional[bool] = True debridService: str debridApiKey: str + debridStreamProxyPassword: Optional[str] = "" @field_validator("indexers") def check_indexers(cls, v, values): diff --git a/poetry.lock b/poetry.lock index 33883a8..57ee1f5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2279,4 +2279,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "ddb74ca11fc846b4142969b7027d42c765ef58952405c241fb759cf5e9f2df53" +content-hash = "24f9bb0c3506c9138b89c473f41e1633b6c41b05274bc4d3dc67c4688a4a1b82"