diff --git a/.env-sample b/.env-sample
index 69904d3..3f7c294 100644
--- a/.env-sample
+++ b/.env-sample
@@ -27,4 +27,5 @@ PROXY_DEBRID_STREAM_DEBRID_DEFAULT_SERVICE=realdebrid # if you want your users w
PROXY_DEBRID_STREAM_DEBRID_DEFAULT_APIKEY=CHANGE_ME # if you want your users who use the Debrid Stream Proxy not to have to specify Debrid information, but to use the default one instead
TITLE_MATCH_CHECK=True # disable if you only use Torrentio / MediaFusion and are sure you're only scraping good titles, for example (keep it True if Zilean is enabled)
REMOVE_ADULT_CONTENT=False # detect and remove adult content
+STREMTHRU_DEFAULT_URL=None # if you want your users to use StremThru without having to specify it
CUSTOM_HEADER_HTML=None # only set it if you know what it is
diff --git a/README.md b/README.md
index 3ed4a4e..47f92aa 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,7 @@
- Direct Torrent supported (do not specify a Debrid API Key on the configuration page (webui) to activate it - it will use the cached results of other users using debrid service)
- [Kitsu](https://kitsu.io/) support (anime)
- Adult Content Filter
+- [StremThru](https://github.com/MunifTanjim/stremthru) support
# Installation
To customize your Comet experience to suit your needs, please first take a look at all the [environment variables](https://github.com/g0ldyy/comet/blob/main/.env-sample)!
diff --git a/comet/api/core.py b/comet/api/core.py
index 1dabf54..fea17a0 100644
--- a/comet/api/core.py
+++ b/comet/api/core.py
@@ -47,6 +47,7 @@ async def configure(request: Request):
"webConfig": web_config,
"indexerManager": settings.INDEXER_MANAGER_TYPE,
"proxyDebridStream": settings.PROXY_DEBRID_STREAM,
+ "stremthruDefaultUrl": settings.STREMTHRU_DEFAULT_URL or "",
},
)
@@ -58,7 +59,7 @@ async def manifest(b64config: str = None):
if not config:
config = {"debridService": None}
- debrid_extension = get_debrid_extension(config["debridService"])
+ debrid_extension = get_debrid_extension(config["debridService"], config["debridApiKey"])
return {
"id": settings.ADDON_ID,
diff --git a/comet/api/stream.py b/comet/api/stream.py
index 53bcd92..7efb77e 100644
--- a/comet/api/stream.py
+++ b/comet/api/stream.py
@@ -32,6 +32,7 @@
get_aliases,
add_torrent_to_cache,
)
+from comet.utils.config import is_proxy_stream_authed, is_proxy_stream_enabled, prepare_debrid_config, should_skip_proxy_stream
from comet.utils.logger import logger
from comet.utils.models import database, rtn, settings, trackers
@@ -135,19 +136,10 @@ async def stream(
if type == "series":
log_name = f"{name} S{season:02d}E{episode:02d}"
- if (
- settings.PROXY_DEBRID_STREAM
- and settings.PROXY_DEBRID_STREAM_PASSWORD
- == config["debridStreamProxyPassword"]
- and config["debridApiKey"] == ""
- ):
- config["debridService"] = (
- settings.PROXY_DEBRID_STREAM_DEBRID_DEFAULT_SERVICE
- )
- config["debridApiKey"] = settings.PROXY_DEBRID_STREAM_DEBRID_DEFAULT_APIKEY
+ prepare_debrid_config(config)
if config["debridApiKey"] == "":
- services = ["realdebrid", "alldebrid", "premiumize", "torbox", "debridlink"]
+ services = ["realdebrid", "alldebrid", "premiumize", "torbox", "debridlink", "stremthru"]
debrid_emoji = "⬇️"
else:
services = [config["debridService"]]
@@ -155,10 +147,8 @@ async def stream(
results = []
if (
- config["debridStreamProxyPassword"] != ""
- and settings.PROXY_DEBRID_STREAM
- and settings.PROXY_DEBRID_STREAM_PASSWORD
- != config["debridStreamProxyPassword"]
+ is_proxy_stream_enabled(config)
+ and not is_proxy_stream_authed(config)
):
results.append(
{
@@ -245,7 +235,7 @@ async def stream(
)
else:
the_stream["infoHash"] = hash
- index = data["index"]
+ index = str(data["index"])
the_stream["fileIdx"] = (
1 if "|" in index else int(index)
) # 1 because for Premiumize it's impossible to get the file index
@@ -421,6 +411,7 @@ async def stream(
season,
episode,
kitsu,
+ video_id=full_id,
)
ranked_files = set()
@@ -474,16 +465,14 @@ async def stream(
logger.info(f"Results have been cached for {log_name}")
- debrid_extension = get_debrid_extension(config["debridService"])
+ debrid_extension = get_debrid_extension(config["debridService"], config["debridApiKey"])
balanced_hashes = get_balanced_hashes(sorted_ranked_files, config)
results = []
if (
- config["debridStreamProxyPassword"] != ""
- and settings.PROXY_DEBRID_STREAM
- and settings.PROXY_DEBRID_STREAM_PASSWORD
- != config["debridStreamProxyPassword"]
+ is_proxy_stream_enabled(config)
+ and not is_proxy_stream_authed(config)
):
results.append(
{
@@ -496,13 +485,17 @@ async def stream(
for resolution in balanced_hashes:
for hash in balanced_hashes[resolution]:
data = sorted_ranked_files[hash]["data"]
+ index = data['index']
+ if index == -1:
+ index = data['title']
+ url = f"{request.url.scheme}://{request.url.netloc}/{b64config}/playback/{hash}/{index}"
results.append(
{
"name": f"[{debrid_extension}⚡] Comet {data['resolution']}",
"description": format_title(data, config),
"torrentTitle": data["torrent_title"],
"torrentSize": data["torrent_size"],
- "url": f"{request.url.scheme}://{request.url.netloc}/{b64config}/playback/{hash}/{data['index']}",
+ "url": url,
"behaviorHints": {
"filename": data["raw_title"],
"bingeGroup": "comet|" + hash,
@@ -545,13 +538,7 @@ async def playback(request: Request, b64config: str, hash: str, index: str):
if not config:
return FileResponse("comet/assets/invalidconfig.mp4")
- if (
- settings.PROXY_DEBRID_STREAM
- and settings.PROXY_DEBRID_STREAM_PASSWORD == config["debridStreamProxyPassword"]
- and config["debridApiKey"] == ""
- ):
- config["debridService"] = settings.PROXY_DEBRID_STREAM_DEBRID_DEFAULT_SERVICE
- config["debridApiKey"] = settings.PROXY_DEBRID_STREAM_DEBRID_DEFAULT_APIKEY
+ prepare_debrid_config(config)
async with aiohttp.ClientSession(raise_for_status=True) as session:
# Check for cached download link
@@ -581,9 +568,8 @@ async def playback(request: Request, b64config: str, hash: str, index: str):
config,
ip
if (
- not settings.PROXY_DEBRID_STREAM
- or settings.PROXY_DEBRID_STREAM_PASSWORD
- != config["debridStreamProxyPassword"]
+ not is_proxy_stream_enabled(config)
+ or not is_proxy_stream_authed(config)
)
else "",
)
@@ -603,10 +589,12 @@ async def playback(request: Request, b64config: str, hash: str, index: str):
},
)
+ if should_skip_proxy_stream(config):
+ return RedirectResponse(download_link, status_code=302)
+
if (
- settings.PROXY_DEBRID_STREAM
- and settings.PROXY_DEBRID_STREAM_PASSWORD
- == config["debridStreamProxyPassword"]
+ is_proxy_stream_enabled(config)
+ and is_proxy_stream_authed(config)
):
if settings.PROXY_DEBRID_STREAM_MAX_CONNECTIONS != -1:
active_ip_connections = await database.fetch_all(
diff --git a/comet/debrid/alldebrid.py b/comet/debrid/alldebrid.py
index 75a06e0..cb433a8 100644
--- a/comet/debrid/alldebrid.py
+++ b/comet/debrid/alldebrid.py
@@ -44,7 +44,7 @@ async def get_instant(self, chunk: list):
)
async def get_files(
- self, torrent_hashes: list, type: str, season: str, episode: str, kitsu: bool
+ self, torrent_hashes: list, type: str, season: str, episode: str, kitsu: bool, **kwargs
):
chunk_size = 500
chunks = [
diff --git a/comet/debrid/debridlink.py b/comet/debrid/debridlink.py
index b3f7261..1d65bd2 100644
--- a/comet/debrid/debridlink.py
+++ b/comet/debrid/debridlink.py
@@ -48,7 +48,7 @@ async def get_instant(self, chunk: list):
return responses
async def get_files(
- self, torrent_hashes: list, type: str, season: str, episode: str, kitsu: bool
+ self, torrent_hashes: list, type: str, season: str, episode: str, kitsu: bool, **kwargs
):
chunk_size = 10
chunks = [
diff --git a/comet/debrid/manager.py b/comet/debrid/manager.py
index cbcc5ed..3646dda 100644
--- a/comet/debrid/manager.py
+++ b/comet/debrid/manager.py
@@ -1,15 +1,28 @@
import aiohttp
+from comet.utils.config import should_use_stremthru
+
from .realdebrid import RealDebrid
from .alldebrid import AllDebrid
from .premiumize import Premiumize
from .torbox import TorBox
from .debridlink import DebridLink
+from .stremthru import StremThru
def getDebrid(session: aiohttp.ClientSession, config: dict, ip: str):
debrid_service = config["debridService"]
debrid_api_key = config["debridApiKey"]
+
+ if should_use_stremthru(config):
+ return StremThru(
+ session=session,
+ url=config["stremthruUrl"],
+ debrid_service=debrid_service,
+ token=debrid_api_key,
+ ip=ip,
+ )
+
if debrid_service == "realdebrid":
return RealDebrid(session, debrid_api_key, ip)
elif debrid_service == "alldebrid":
diff --git a/comet/debrid/premiumize.py b/comet/debrid/premiumize.py
index 6631e14..27fdc93 100644
--- a/comet/debrid/premiumize.py
+++ b/comet/debrid/premiumize.py
@@ -49,7 +49,7 @@ async def get_instant(self, chunk: list):
)
async def get_files(
- self, torrent_hashes: list, type: str, season: str, episode: str, kitsu: bool
+ self, torrent_hashes: list, type: str, season: str, episode: str, kitsu: bool, **kwargs
):
chunk_size = 100
chunks = [
diff --git a/comet/debrid/realdebrid.py b/comet/debrid/realdebrid.py
index d37c864..33b993e 100644
--- a/comet/debrid/realdebrid.py
+++ b/comet/debrid/realdebrid.py
@@ -42,7 +42,7 @@ async def get_instant(self, chunk: list):
)
async def get_files(
- self, torrent_hashes: list, type: str, season: str, episode: str, kitsu: bool
+ self, torrent_hashes: list, type: str, season: str, episode: str, kitsu: bool, **kwargs
):
chunk_size = 100
chunks = [
diff --git a/comet/debrid/stremthru.py b/comet/debrid/stremthru.py
new file mode 100644
index 0000000..5b58734
--- /dev/null
+++ b/comet/debrid/stremthru.py
@@ -0,0 +1,200 @@
+import asyncio
+from typing import Optional
+
+import aiohttp
+from RTN import parse
+
+from comet.utils.general import is_video
+from comet.utils.logger import logger
+
+
+class StremThru:
+ def __init__(
+ self,
+ session: aiohttp.ClientSession,
+ url: str,
+ token: str,
+ debrid_service: str,
+ ip: str,
+ ):
+ if not self.is_supported_store(debrid_service):
+ raise ValueError(f"unsupported store: {debrid_service}")
+
+ store, token = self.parse_store_creds(debrid_service, token)
+ if store == "stremthru":
+ session.headers["Proxy-Authorization"] = f"Basic {token}"
+ else:
+ session.headers["X-StremThru-Store-Name"] = store
+ session.headers["X-StremThru-Store-Authorization"] = f"Bearer {token}"
+
+ session.headers["User-Agent"] = "comet"
+
+ self.session = session
+ self.base_url = f"{url}/v0/store"
+ self.name = f"StremThru[{debrid_service}]" if debrid_service else "StremThru"
+ self.client_ip = ip
+
+ @staticmethod
+ def parse_store_creds(debrid_service, token: str = ""):
+ if debrid_service != "stremthru":
+ return debrid_service, token
+ if ":" in token:
+ parts = token.split(":")
+ return parts[0], parts[1]
+ return debrid_service, token
+
+ @staticmethod
+ def is_supported_store(name: Optional[str]):
+ return (
+ name == "stremthru"
+ or name == "alldebrid"
+ or name == "debridlink"
+ or name == "easydebrid"
+ or name == "premiumize"
+ or name == "realdebrid"
+ or name == "torbox"
+ )
+
+ async def check_premium(self):
+ try:
+ user = await self.session.get(
+ f"{self.base_url}/user?client_ip={self.client_ip}"
+ )
+ user = await user.json()
+ return user["data"]["subscription_status"] == "premium"
+ except Exception as e:
+ logger.warning(
+ f"Exception while checking premium status on {self.name}: {e}"
+ )
+
+ return False
+
+ async def get_instant(self, magnets: list, sid: Optional[str] = None):
+ try:
+ url = f"{self.base_url}/magnets/check?magnet={','.join(magnets)}&client_ip={self.client_ip}"
+ if sid:
+ url = f"{url}&sid={sid}"
+ magnet = await self.session.get(url)
+ return await magnet.json()
+ except Exception as e:
+ logger.warning(
+ f"Exception while checking hash instant availability on {self.name}: {e}"
+ )
+
+ async def get_files(
+ self,
+ torrent_hashes: list,
+ type: str,
+ season: str,
+ episode: str,
+ kitsu: bool,
+ video_id: Optional[str] = None,
+ **kwargs,
+ ):
+ chunk_size = 25
+ chunks = [
+ torrent_hashes[i : i + chunk_size]
+ for i in range(0, len(torrent_hashes), chunk_size)
+ ]
+
+ tasks = []
+ for chunk in chunks:
+ tasks.append(self.get_instant(chunk, sid=video_id))
+
+ responses = await asyncio.gather(*tasks)
+
+ availability = [
+ response["data"]["items"]
+ for response in responses
+ if response and "data" in response
+ ]
+
+ files = {}
+
+ if type == "series":
+ for magnets in availability:
+ for magnet in magnets:
+ if magnet["status"] != "cached":
+ continue
+
+ for file in magnet["files"]:
+ filename = file["name"]
+
+ if not is_video(filename) or "sample" in filename:
+ continue
+
+ filename_parsed = parse(filename)
+
+ if episode not in filename_parsed.episodes:
+ continue
+
+ if kitsu:
+ if filename_parsed.seasons:
+ continue
+ else:
+ if season not in filename_parsed.seasons:
+ continue
+
+ files[magnet["hash"]] = {
+ "index": file["index"],
+ "title": filename,
+ "size": file["size"],
+ }
+
+ break
+ else:
+ for magnets in availability:
+ for magnet in magnets:
+ if magnet["status"] != "cached":
+ continue
+
+ for file in magnet["files"]:
+ filename = file["name"]
+
+ if not is_video(filename) or "sample" in filename:
+ continue
+
+ files[magnet["hash"]] = {
+ "index": file["index"],
+ "title": filename,
+ "size": file["size"],
+ }
+
+ break
+
+ return files
+
+ async def generate_download_link(self, hash: str, index: str):
+ try:
+ magnet = await self.session.post(
+ f"{self.base_url}/magnets?client_ip={self.client_ip}",
+ json={"magnet": f"magnet:?xt=urn:btih:{hash}"},
+ )
+ magnet = await magnet.json()
+
+ if magnet["data"]["status"] != "downloaded":
+ return
+
+ file = next(
+ (
+ file
+ for file in magnet["data"]["files"]
+ if str(file["index"]) == index or file["name"] == index
+ ),
+ None,
+ )
+
+ if not file:
+ return
+
+ link = await self.session.post(
+ f"{self.base_url}/link/generate?client_ip={self.client_ip}",
+ json={"link": file["link"]},
+ )
+ link = await link.json()
+
+ return link["data"]["link"]
+ except Exception as e:
+ logger.warning(
+ f"Exception while getting download link from {self.name} for {hash}|{index}: {e}"
+ )
diff --git a/comet/debrid/torbox.py b/comet/debrid/torbox.py
index 2e9cd6d..7e17054 100644
--- a/comet/debrid/torbox.py
+++ b/comet/debrid/torbox.py
@@ -41,7 +41,7 @@ async def get_instant(self, chunk: list):
)
async def get_files(
- self, torrent_hashes: list, type: str, season: str, episode: str, kitsu: bool
+ self, torrent_hashes: list, type: str, season: str, episode: str, kitsu: bool, **kwargs
):
chunk_size = 50
chunks = [
diff --git a/comet/main.py b/comet/main.py
index 4cccd79..7a732bc 100644
--- a/comet/main.py
+++ b/comet/main.py
@@ -156,6 +156,8 @@ def start_log():
"COMET",
f"Debrid Stream Proxy: {bool(settings.PROXY_DEBRID_STREAM)} - Password: {settings.PROXY_DEBRID_STREAM_PASSWORD} - Max Connections: {settings.PROXY_DEBRID_STREAM_MAX_CONNECTIONS} - Default Debrid Service: {settings.PROXY_DEBRID_STREAM_DEBRID_DEFAULT_SERVICE} - Default Debrid API Key: {settings.PROXY_DEBRID_STREAM_DEBRID_DEFAULT_APIKEY}",
)
+ if settings.STREMTHRU_DEFAULT_URL:
+ logger.log("COMET", f"Default StremThru URL: {settings.STREMTHRU_DEFAULT_URL}")
logger.log("COMET", f"Title Match Check: {bool(settings.TITLE_MATCH_CHECK)}")
logger.log("COMET", f"Remove Adult Content: {bool(settings.REMOVE_ADULT_CONTENT)}")
logger.log("COMET", f"Custom Header HTML: {bool(settings.CUSTOM_HEADER_HTML)}")
diff --git a/comet/templates/index.html b/comet/templates/index.html
index 75b6ccc..2326ade 100644
--- a/comet/templates/index.html
+++ b/comet/templates/index.html
@@ -546,6 +546,7 @@