From b5b48f3b80e6025a95a69aabe9a630a122a7a2f6 Mon Sep 17 00:00:00 2001 From: Goldy <153996346+g0ldyy@users.noreply.github.com> Date: Tue, 11 Jun 2024 23:38:52 +0200 Subject: [PATCH] Add files via upload --- main.py | 359 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 359 insertions(+) create mode 100644 main.py diff --git a/main.py b/main.py new file mode 100644 index 0000000..5391b0c --- /dev/null +++ b/main.py @@ -0,0 +1,359 @@ +import aiohttp, asyncio, bencodepy, hashlib, re, base64, json, dotenv, os +from fastapi import FastAPI, Request +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import RedirectResponse + +dotenv.load_dotenv() + +downloadLinks = {} # temporary before sqlite cache db is implemented + +app = FastAPI(docs_url=None) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +def configChecking(b64config: str): + try: + config = json.loads(base64.b64decode(b64config).decode()) + + if config["debridService"] not in ["realdebrid"]: + return False + + if not isinstance(config["debridApiKey"], str): + return False + + if not isinstance(config["indexers"], list): + return False + + if not isinstance(config["maxResults"], (int)): + return False + + return config + except: + return False + +@app.get("/manifest.json") +@app.get("/{b64config}/manifest.json") +async def manifest(b64config: str): + if not configChecking(b64config): + return + + return { + "id": "stremio.comet.fast", + "version": "1.0.0", + "name": "Comet", + "description": "Stremio's fastest torrent/debrid search add-on.", + "icon": "https://i.imgur.com/cZOiNzX.jpeg", + "logo": "https://i.imgur.com/cZOiNzX.jpeg", + "resources": [ + "stream" + ], + "types": [ + "movie", + "series" + ], + "idPrefixes": [ + "tt" + ], + "catalogs": [], + "behaviorHints": { + "configurable": True + } + } + +async def getJackett(session: aiohttp.ClientSession, indexers: list, query: str): + response = await session.get(f"{os.getenv('JACKETT_URL')}/api/v2.0/indexers/all/results?apikey={os.getenv('JACKETT_KEY')}&Query={query}&Tracker[]={'&Tracker[]='.join(indexer for indexer in indexers)}") + return response + +async def getTorrentHash(session: aiohttp.ClientSession, url: str): + try: + timeout = aiohttp.ClientTimeout(total=int(os.getenv("GET_TORRENT_TIMEOUT"))) + response = await session.get(url, allow_redirects=False, timeout=timeout) + if response.status == 200: + torrentData = await response.read() + torrentDict = bencodepy.decode(torrentData) + info = bencodepy.encode(torrentDict[b"info"]) + hash = hashlib.sha1(info).hexdigest() + else: + location = response.headers.get("Location", "") + if not location: + return + + match = re.search("btih:([a-zA-Z0-9]+)", location) + if not match: + return + + hash = match.group(1).upper() + + return hash + except: + pass + +@app.get("/stream/{type}/{id}.json") +@app.get("/{b64config}/stream/{type}/{id}.json") +async def stream(request: Request, b64config: str, type: str, id: str): + config = configChecking(b64config) + if not config: + return + + async with aiohttp.ClientSession() as session: + checkDebrid = await session.get("https://api.real-debrid.com/rest/1.0/user", headers={ + "Authorization": f"Bearer {config['debridApiKey']}" + }) + checkDebrid = await checkDebrid.text() + if not '"type": "premium"' in checkDebrid: + return { + "streams": [ + { + "name": f"[⚠️] Comet", + "title": f"Invalid Real-Debrid account.", + "url": f"https://comet.fast" + } + ] + } + + if type == "series": + info = id.split(":") + + id = info[0] + season = info[1].zfill(2) + episode = info[2].zfill(2) + + getMetadata = await session.get(f"https://v3.sg.media-imdb.com/suggestion/a/{id}.json") + metadata = await getMetadata.json() + + name = metadata["d"][0]["l"] + toChange = { + 'ā': 'a', 'ă': 'a', 'ą': 'a', 'ć': 'c', 'č': 'c', 'ç': 'c', + 'ĉ': 'c', 'ċ': 'c', 'ď': 'd', 'đ': 'd', 'è': 'e', 'é': 'e', + 'ê': 'e', 'ë': 'e', 'ē': 'e', 'ĕ': 'e', 'ę': 'e', 'ě': 'e', + 'ĝ': 'g', 'ğ': 'g', 'ġ': 'g', 'ģ': 'g', 'ĥ': 'h', 'î': 'i', + 'ï': 'i', 'ì': 'i', 'í': 'i', 'ī': 'i', 'ĩ': 'i', 'ĭ': 'i', + 'ı': 'i', 'ĵ': 'j', 'ķ': 'k', 'ĺ': 'l', 'ļ': 'l', 'ł': 'l', + 'ń': 'n', 'ň': 'n', 'ñ': 'n', 'ņ': 'n', 'ʼn': 'n', 'ó': 'o', + 'ô': 'o', 'õ': 'o', 'ö': 'o', 'ø': 'o', 'ō': 'o', 'ő': 'o', + 'œ': 'oe', 'ŕ': 'r', 'ř': 'r', 'ŗ': 'r', 'š': 's', 'ş': 's', + 'ś': 's', 'ș': 's', 'ß': 'ss', 'ť': 't', 'ţ': 't', 'ū': 'u', + 'ŭ': 'u', 'ũ': 'u', 'û': 'u', 'ü': 'u', 'ù': 'u', 'ú': 'u', + 'ų': 'u', 'ű': 'u', 'ŵ': 'w', 'ý': 'y', 'ÿ': 'y', 'ŷ': 'y', + 'ž': 'z', 'ż': 'z', 'ź': 'z', 'æ': 'ae', 'ǎ': 'a', 'ǧ': 'g', + 'ə': 'e', 'ƒ': 'f', 'ǐ': 'i', 'ǒ': 'o', 'ǔ': 'u', 'ǚ': 'u', + 'ǜ': 'u', 'ǹ': 'n', 'ǻ': 'a', 'ǽ': 'ae', 'ǿ': 'o', + } + translationTable = str.maketrans(toChange) + name = name.translate(translationTable) + + print(f"Start of Jackett search for {name}") + + tasks = [] + tasks.append(getJackett(session, config["indexers"], name)) + if type == "series": + tasks.append(getJackett(session, config["indexers"], f"{name} S{season}E{episode}")) + jackettSearchResponses = await asyncio.gather(*tasks) + + torrents = [] + for response in jackettSearchResponses: + results = await response.json() + for i in results["Results"]: + torrents.append(i) + + print(f"{len(torrents)} torrents found for {name}") + + if len(torrents) == 0: + return {"streams": []} + + tasks = [] + for torrent in torrents: + tasks.append(getTorrentHash(session, torrent["Link"])) + torrentHashes = await asyncio.gather(*tasks) + torrentHashes = list(set([hash for hash in torrentHashes if hash])) + + print(f"{len(torrentHashes)} info hashes found for {name}") + + if len(torrentHashes) == 0: + return {"streams": []} + + getAvailability = await session.get(f"https://api.real-debrid.com/rest/1.0/torrents/instantAvailability/{'/'.join(torrentHashes)}", headers={ + "Authorization": f"Bearer {config['debridApiKey']}" + }) + + files = {} + strictFiles = {} + + availability = await getAvailability.json() + for hash, details in availability.items(): + if not "rd" in details: + continue + + if type == "series": + for variants in details["rd"]: + for index, file in variants.items(): + filename = file["filename"].lower() + + if not filename.endswith(tuple([".mkv", ".mp4", ".avi", ".mov", ".flv", ".wmv", ".webm", ".mpg", ".mpeg", ".m4v", ".3gp", ".3g2", ".ogv", ".ogg", ".drc", ".gif", ".gifv", ".mng", ".avi", ".mov", ".qt", ".wmv", ".yuv", ".rm", ".rmvb", ".asf", ".amv", ".m4p", ".m4v", ".mpg", ".mp2", ".mpeg", ".mpe", ".mpv", ".mpg", ".mpeg", ".m2v", ".m4v", ".svi", ".3gp", ".3g2", ".mxf", ".roq", ".nsv", ".flv", ".f4v", ".f4p", ".f4a", ".f4b"])): + continue + + if season in filename and episode in filename: + files[hash] = { + "index": index, + "title": file["filename"], + "size": file["filesize"] + } + + if f"s{season}" in filename and f"e{episode}" in filename: + strictFiles[hash] = { + "index": index, + "title": file["filename"], + "size": file["filesize"] + } + + continue + + for variants in details["rd"]: + for index, file in variants.items(): + filename = file["filename"].lower() + if not filename.endswith(tuple([".mkv", ".mp4", ".avi", ".mov", ".flv", ".wmv", ".webm", ".mpg", ".mpeg", ".m4v", ".3gp", ".3g2", ".ogv", ".ogg", ".drc", ".gif", ".gifv", ".mng", ".avi", ".mov", ".qt", ".wmv", ".yuv", ".rm", ".rmvb", ".asf", ".amv", ".m4p", ".m4v", ".mpg", ".mp2", ".mpeg", ".mpe", ".mpv", ".mpg", ".mpeg", ".m2v", ".m4v", ".svi", ".3gp", ".3g2", ".mxf", ".roq", ".nsv", ".flv", ".f4v", ".f4p", ".f4a", ".f4b"])): + continue + + files[hash] = { + "index": index, + "title": file["filename"], + "size": file["filesize"] + } + + if len(strictFiles) > 0: + files = strictFiles + + print(f"{len(files)} cached files found on Real-Debrid for {name}") + + if len(files) == 0: + return {"streams": []} + + files = dict(sorted(files.items(), key=lambda item: item[1]["size"], reverse=True)) + + filesByResolution = {"4k": [], "1080p": [], "720p": [], "480p": [], "Unknown": []} + for key, value in files.items(): + qualityPattern = ( + (r"\b(2160P|UHD|4K)\b", "4k"), + (r"\b(1080P|FHD|FULLHD|HD|HIGHDEFINITION)\b", "1080p"), + (r"\b(720P|HD|HIGHDEFINITION)\b", "720p"), + (r"\b(480P|SD|STANDARDDEFINITION)\b", "480p"), + ) + + resolution = "Unknown" + for pattern, quality in qualityPattern: + if re.search(pattern, value["title"], re.IGNORECASE): + resolution = quality + + filesByResolution[resolution].append({ + key: value + }) + + hashCount = 0 + for quality in filesByResolution: + hashCount += len(filesByResolution[quality]) + + results = [] + if hashCount <= config["maxResults"] or config["maxResults"] == 0: + for quality, files in filesByResolution.items(): + for file in files: + for hash in file: + results.append({ + "name": f"[RD⚡] Comet {quality}", + "title": f"{file[hash]['title']}\n💾 {round(int(file[hash]['size']) / 1024 / 1024 / 1024, 2)}GB", + "url": f"{request.url.scheme}://{request.url.netloc}/{b64config}/playback/{hash}/{file[hash]['index']}" + }) + else: + selectedFiles = [] + resolutionCount = {res: 0 for res in filesByResolution.keys()} + resolutions = list(filesByResolution.keys()) + + while len(selectedFiles) < config["maxResults"]: + for resolution in resolutions: + if len(selectedFiles) >= config["maxResults"]: + break + if resolutionCount[resolution] < len(filesByResolution[resolution]): + selectedFiles.append((resolution, filesByResolution[resolution][resolutionCount[resolution]])) + resolutionCount[resolution] += 1 + + balancedFiles = {res: [] for res in filesByResolution.keys()} + for resolution, file in selectedFiles: + balancedFiles[resolution].append(file) + + for quality, files in balancedFiles.items(): + for file in files: + for hash in file: + results.append({ + "name": f"[RD⚡] Comet {quality}", + "title": f"{file[hash]['title']}\n💾 {round(int(file[hash]['size']) / 1024 / 1024 / 1024, 2)}GB", + "url": f"{request.url.scheme}://{request.url.netloc}/{b64config}/playback/{hash}/{file[hash]['index']}" + }) + + return { + "streams": results + } + +async def generateDownloadLink(session, debridApiKey: str, hash: str, index: str): + addMagnet = await session.post(f"https://api.real-debrid.com/rest/1.0/torrents/addMagnet", headers={ + "Authorization": f"Bearer {debridApiKey}" + }, data={ + "magnet": f"magnet:?xt=urn:btih:{hash}" + }) + addMagnet = await addMagnet.json() + + getMagnetInfo = await session.get(addMagnet["uri"], headers={ + "Authorization": f"Bearer {debridApiKey}" + }) + getMagnetInfo = await getMagnetInfo.json() + + selectFile = await session.post(f"https://api.real-debrid.com/rest/1.0/torrents/selectFiles/{addMagnet['id']}", headers={ + "Authorization": f"Bearer {debridApiKey}" + }, data={ + "files": index + }) + + getMagnetInfo = await session.get(addMagnet["uri"], headers={ + "Authorization": f"Bearer {debridApiKey}" + }) + getMagnetInfo = await getMagnetInfo.json() + + unrestrictLink = await session.post(f"https://api.real-debrid.com/rest/1.0/unrestrict/link", headers={ + "Authorization": f"Bearer {debridApiKey}" + }, data={ + "link": getMagnetInfo["links"][0] + }) + unrestrictLink = await unrestrictLink.json() + + return unrestrictLink["download"] + +@app.head("/{b64config}/playback/{hash}/{index}") +async def stream(b64config: str, hash: str, index: str): + config = configChecking(b64config) + if not config: + return + + async with aiohttp.ClientSession() as session: + downloadLink = await generateDownloadLink(session, config["debridApiKey"], hash, index) + downloadLinks[(hash, index)] = downloadLink + + return RedirectResponse(downloadLink, status_code=302) + +@app.get("/{b64config}/playback/{hash}/{index}") +async def stream(b64config: str, hash: str, index: str): + config = configChecking(b64config) + if not config: + return + + async with aiohttp.ClientSession() as session: + if (hash, index) in downloadLinks: + downloadLink = downloadLinks[(hash, index)] + else: + downloadLink = await generateDownloadLink(session, config["debridApiKey"], hash, index) + downloadLinks[(hash, index)] = downloadLink + + return RedirectResponse(downloadLink, status_code=302) \ No newline at end of file