mirror of
https://github.com/g0ldyy/comet.git
synced 2026-01-12 01:16:12 +01:00
@@ -2,6 +2,8 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
logs/
|
||||
.vscode/
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from loguru import logger
|
||||
|
||||
|
||||
def setup_logger(level):
|
||||
"""Setup the logger"""
|
||||
logs_dir_path = "logs"
|
||||
os.makedirs(logs_dir_path, exist_ok=True)
|
||||
timestamp = datetime.now().strftime("%Y%m%d-%H%M")
|
||||
log_filename = Path(logs_dir_path) / f"comet-{timestamp}.log"
|
||||
|
||||
logger.level("PROGRAM", no=36, color="<blue>", icon="☄️ ")
|
||||
logger.level("DEBRID", no=38, color="<fg #DC5F00>", icon="👻")
|
||||
logger.level("SCRAPER", no=40, color="<magenta>", icon="🌠")
|
||||
|
||||
# set the default info and debug level icons
|
||||
logger.level("INFO", icon="📰", color="<fg #DC5F00>")
|
||||
logger.level("DEBUG", icon="🕸️", color="<fg #DC5F00>")
|
||||
logger.level("WARNING", icon="⚠️ ", color="<fg #DC5F00>")
|
||||
|
||||
# Log format to match the old log format, but with color
|
||||
log_format = (
|
||||
"<white>{time:YYYY-MM-DD}</white> <magenta>{time:HH:mm:ss}</magenta> | "
|
||||
"<level>{level.icon}</level> <level>{level: <9}</level> | "
|
||||
"<cyan>{module}</cyan>.<cyan>{function}</cyan> - <level>{message}</level>"
|
||||
)
|
||||
|
||||
logger.configure(handlers=[
|
||||
{
|
||||
"sink": sys.stderr,
|
||||
"level": "DEBUG",
|
||||
"format": log_format,
|
||||
"backtrace": False,
|
||||
"diagnose": False,
|
||||
"enqueue": True,
|
||||
},
|
||||
{
|
||||
"sink": log_filename,
|
||||
"level": level,
|
||||
"format": log_format,
|
||||
"rotation": "1 hour",
|
||||
"retention": "1 hour",
|
||||
"compression": None,
|
||||
"backtrace": False,
|
||||
"diagnose": True,
|
||||
"enqueue": True,
|
||||
},
|
||||
])
|
||||
|
||||
|
||||
def clean_old_logs():
|
||||
"""Remove old log files based on retention settings."""
|
||||
try:
|
||||
logs_dir_path = Path("logs")
|
||||
for log_file in logs_dir_path.glob("comet-*.log"):
|
||||
# remove files older than 1 hour so that logs dont get messy
|
||||
if (datetime.now() - datetime.fromtimestamp(log_file.stat().st_mtime)).total_seconds() / 3600 > 1:
|
||||
log_file.unlink()
|
||||
logger.log("PROGRAM", f"Old log file {log_file.name} removed.")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to clean old logs: {e}")
|
||||
|
||||
setup_logger("DEBUG")
|
||||
clean_old_logs()
|
||||
+247
-213
@@ -1,14 +1,25 @@
|
||||
import aiohttp, asyncio, bencodepy, hashlib, re, base64, json, dotenv, os
|
||||
from fastapi import FastAPI, Request
|
||||
import asyncio
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
import aiohttp
|
||||
import bencodepy
|
||||
from fastapi import FastAPI, HTTPException, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import RedirectResponse
|
||||
from pydantic import ValidationError
|
||||
from RTN import parse
|
||||
|
||||
dotenv.load_dotenv()
|
||||
from comet.logger import logger
|
||||
from comet.models import ConfigModel, settings, video_extensions
|
||||
|
||||
downloadLinks = {} # temporary before sqlite cache db is implemented
|
||||
|
||||
app = FastAPI(docs_url=None)
|
||||
|
||||
app = FastAPI()
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
@@ -17,31 +28,29 @@ app.add_middleware(
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
def configChecking(b64config: str):
|
||||
try:
|
||||
config = json.loads(base64.b64decode(b64config).decode())
|
||||
decoded_config = base64.b64decode(b64config).decode()
|
||||
config = json.loads(decoded_config)
|
||||
logger.info("Configuration decoded successfully")
|
||||
|
||||
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:
|
||||
validated_config = ConfigModel(**config)
|
||||
logger.debug(validated_config)
|
||||
logger.info("Configuration validated successfully")
|
||||
return validated_config.model_dump()
|
||||
except ValidationError as e:
|
||||
logger.error("Validation error in configChecking: %s", str(e))
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error("Error in configChecking: %s", str(e))
|
||||
return False
|
||||
|
||||
@app.get("/manifest.json")
|
||||
@app.get("/{b64config}/manifest.json")
|
||||
async def manifest(b64config: str):
|
||||
if not configChecking(b64config):
|
||||
return
|
||||
@app.get("/{config_str}/manifest.json")
|
||||
async def manifest(request: Request, config_str: str = None):
|
||||
if config_str and not configChecking(config_str):
|
||||
raise HTTPException(status_code=400, detail="Invalid configuration")
|
||||
|
||||
return {
|
||||
"id": "stremio.comet.fast",
|
||||
@@ -67,12 +76,12 @@ async def manifest(b64config: str):
|
||||
}
|
||||
|
||||
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)}")
|
||||
response = await session.get(f"{settings.JACKETT_URL}/api/v2.0/indexers/all/results?apikey={settings.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")))
|
||||
timeout = aiohttp.ClientTimeout(total=settings.GET_TORRENT_TIMEOUT)
|
||||
response = await session.get(url, allow_redirects=False, timeout=timeout)
|
||||
if response.status == 200:
|
||||
torrentData = await response.read()
|
||||
@@ -87,217 +96,242 @@ async def getTorrentHash(session: aiohttp.ClientSession, url: str):
|
||||
match = re.search("btih:([a-zA-Z0-9]+)", location)
|
||||
if not match:
|
||||
return
|
||||
|
||||
|
||||
hash = match.group(1).upper()
|
||||
|
||||
return hash
|
||||
except:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get torrent hash: {e}")
|
||||
|
||||
@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
|
||||
@app.get("/{config_str}/stream/{type}/{id}.json")
|
||||
async def stream(request: Request, type: str, id: str, config_str: str = None):
|
||||
if config_str:
|
||||
config = configChecking(config_str)
|
||||
if not config:
|
||||
raise HTTPException(status_code=400, detail="Invalid configuration")
|
||||
else:
|
||||
config = None # Handle the case where config_str is not provided
|
||||
|
||||
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 config and not await is_premium_account(session, config.debridApiKey):
|
||||
return invalid_account_response()
|
||||
|
||||
if type == "series":
|
||||
id, season, episode = parse_series_id(id)
|
||||
|
||||
name = await get_metadata_name(session, id)
|
||||
name = translate_name(name)
|
||||
|
||||
logger.debug(f"Start of Jackett search for {name}")
|
||||
|
||||
if type == "series":
|
||||
torrents = await search_torrents(session, config.indexers if config else [], name, type, season, episode)
|
||||
else:
|
||||
torrents = await search_torrents(session, config.indexers if config else [], name, type)
|
||||
logger.debug(f"{len(torrents)} torrents found for {name}")
|
||||
|
||||
if not torrents:
|
||||
return {"streams": []}
|
||||
|
||||
torrentHashes = await get_torrent_hashes(session, torrents)
|
||||
logger.debug(f"{len(torrentHashes)} info hashes found for {name}")
|
||||
|
||||
if not torrentHashes:
|
||||
return {"streams": []}
|
||||
|
||||
files = await get_available_files(session, config.debridApiKey if config else "", torrentHashes, type, season, episode)
|
||||
logger.debug(f"{len(files)} cached files found on Real-Debrid for {name}")
|
||||
|
||||
if not files:
|
||||
return {"streams": []}
|
||||
|
||||
results = generate_stream_results(files, config.maxResults if config else 0, request.url, config_str)
|
||||
return {"streams": results}
|
||||
|
||||
async def is_premium_account(session, debridApiKey: str) -> bool:
|
||||
checkDebrid = await session.get("https://api.real-debrid.com/rest/1.0/user", headers={
|
||||
"Authorization": f"Bearer {debridApiKey}"
|
||||
})
|
||||
checkDebrid = await checkDebrid.text()
|
||||
return '"type": "premium"' in checkDebrid
|
||||
|
||||
def invalid_account_response():
|
||||
return {
|
||||
"streams": [
|
||||
{
|
||||
"name": f"[⚠️] Comet",
|
||||
"title": f"Invalid Real-Debrid account.",
|
||||
"url": f"https://comet.fast"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
def parse_series_id(id: str):
|
||||
info = id.split(":")
|
||||
return info[0], info[1].zfill(2), info[2].zfill(2)
|
||||
|
||||
async def get_metadata_name(session, id: str) -> str:
|
||||
getMetadata = await session.get(f"https://v3.sg.media-imdb.com/suggestion/a/{id}.json")
|
||||
metadata = await getMetadata.json()
|
||||
return metadata["d"][0]["l"]
|
||||
|
||||
def translate_name(name: str) -> str:
|
||||
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)
|
||||
return name.translate(translationTable)
|
||||
|
||||
async def search_torrents(session, indexers: list, name: str, type: str, season: str = None, episode: str = None):
|
||||
tasks = [getJackett(session, indexers, name)]
|
||||
if type == "series":
|
||||
tasks.append(getJackett(session, 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)
|
||||
return torrents
|
||||
|
||||
async def get_torrent_hashes(session, torrents: list):
|
||||
tasks = [getTorrentHash(session, torrent["Link"]) for torrent in torrents]
|
||||
torrentHashes = await asyncio.gather(*tasks)
|
||||
return list(set([hash for hash in torrentHashes if hash]))
|
||||
|
||||
async def get_available_files(session, debridApiKey: str, torrentHashes: list, type: str, season: str = None, episode: str = None):
|
||||
getAvailability = await session.get(f"https://api.real-debrid.com/rest/1.0/torrents/instantAvailability/{'/'.join(torrentHashes)}", headers={
|
||||
"Authorization": f"Bearer {debridApiKey}"
|
||||
})
|
||||
|
||||
files = {}
|
||||
strictFiles = {}
|
||||
|
||||
availability = await getAvailability.json()
|
||||
for hash, details in availability.items():
|
||||
if not "rd" in details:
|
||||
continue
|
||||
|
||||
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"])):
|
||||
|
||||
if not filename.endswith(video_extensions):
|
||||
continue
|
||||
|
||||
files[hash] = {
|
||||
"index": index,
|
||||
"title": file["filename"],
|
||||
"size": file["filesize"]
|
||||
}
|
||||
if season in filename and episode in filename:
|
||||
files[hash] = {
|
||||
"index": index,
|
||||
"title": file["filename"],
|
||||
"size": file["filesize"]
|
||||
}
|
||||
|
||||
if len(strictFiles) > 0:
|
||||
files = strictFiles
|
||||
if f"s{season}" in filename and f"e{episode}" in filename:
|
||||
strictFiles[hash] = {
|
||||
"index": index,
|
||||
"title": file["filename"],
|
||||
"size": file["filesize"]
|
||||
}
|
||||
|
||||
print(f"{len(files)} cached files found on Real-Debrid for {name}")
|
||||
continue
|
||||
|
||||
if len(files) == 0:
|
||||
return {"streams": []}
|
||||
for variants in details["rd"]:
|
||||
for index, file in variants.items():
|
||||
filename = file["filename"].lower()
|
||||
if not filename.endswith(video_extensions):
|
||||
continue
|
||||
|
||||
files[hash] = {
|
||||
"index": index,
|
||||
"title": file["filename"],
|
||||
"size": file["filesize"]
|
||||
}
|
||||
|
||||
if len(strictFiles) > 0:
|
||||
files = strictFiles
|
||||
|
||||
return files
|
||||
|
||||
def generate_stream_results(files, maxResults, url, config_str):
|
||||
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
|
||||
|
||||
files = dict(sorted(files.items(), key=lambda item: item[1]["size"], reverse=True))
|
||||
filesByResolution[resolution].append({
|
||||
key: value
|
||||
})
|
||||
|
||||
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"),
|
||||
)
|
||||
hashCount = 0
|
||||
for quality in filesByResolution:
|
||||
hashCount += len(filesByResolution[quality])
|
||||
|
||||
resolution = "Unknown"
|
||||
for pattern, quality in qualityPattern:
|
||||
if re.search(pattern, value["title"], re.IGNORECASE):
|
||||
resolution = quality
|
||||
|
||||
filesByResolution[resolution].append({
|
||||
key: value
|
||||
})
|
||||
results = []
|
||||
if hashCount <= maxResults or 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"{url.scheme}://{url.netloc}/{config_str}/playback/{hash}/{file[hash]['index']}" if config_str else f"{url.scheme}://{url.netloc}/playback/{hash}/{file[hash]['index']}"
|
||||
})
|
||||
else:
|
||||
selectedFiles = []
|
||||
resolutionCount = {res: 0 for res in filesByResolution.keys()}
|
||||
resolutions = list(filesByResolution.keys())
|
||||
|
||||
while len(selectedFiles) < maxResults:
|
||||
for resolution in resolutions:
|
||||
if len(selectedFiles) >= 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)
|
||||
|
||||
hashCount = 0
|
||||
for quality in filesByResolution:
|
||||
hashCount += len(filesByResolution[quality])
|
||||
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"{url.scheme}://{url.netloc}/{config_str}/playback/{hash}/{file[hash]['index']}" if config_str else f"{url.scheme}://{url.netloc}/playback/{hash}/{file[hash]['index']}"
|
||||
})
|
||||
|
||||
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)
|
||||
return results
|
||||
|
||||
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}"
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
from typing import List
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
JACKETT_URL: str
|
||||
JACKETT_KEY: str
|
||||
GET_TORRENT_TIMEOUT: int
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
env_file_encoding = "utf-8"
|
||||
case_sensitive = False
|
||||
extra = "ignore"
|
||||
validate_assignment = True
|
||||
|
||||
class ConfigModel(BaseModel):
|
||||
debridService: str
|
||||
debridApiKey: str
|
||||
indexers: List[str]
|
||||
maxResults: int
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"debridService": "RealDebrid",
|
||||
"debridApiKey": "your_api_key_here",
|
||||
"indexers": ["indexer1", "indexer2"],
|
||||
"maxResults": 10
|
||||
}
|
||||
}
|
||||
|
||||
video_extensions: 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", ".mp2", ".mpe", ".mpv", ".m2v", ".svi", ".mxf", ".roq", ".nsv",
|
||||
".f4v", ".f4p", ".f4a", ".f4b"
|
||||
)
|
||||
|
||||
settings = Settings()
|
||||
@@ -0,0 +1,13 @@
|
||||
PHONY: run install sort
|
||||
|
||||
|
||||
install:
|
||||
@poetry install --no-root --group dev
|
||||
|
||||
run:
|
||||
@echo Visit http://127.0.0.1:8000/docs for the API Documentation
|
||||
@echo Visit http://127.0.0.1:8000/manifest for the Stremio Manifest
|
||||
@poetry run uvicorn comet.main:app --reload --reload-dir comet --reload-include "*.py" --reload-exclude "*.log"
|
||||
|
||||
sort:
|
||||
@poetry run isort comet
|
||||
Generated
+67
-2
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
@@ -517,6 +517,20 @@ files = [
|
||||
{file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "isort"
|
||||
version = "5.13.2"
|
||||
description = "A Python utility / library to sort Python imports."
|
||||
optional = false
|
||||
python-versions = ">=3.8.0"
|
||||
files = [
|
||||
{file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"},
|
||||
{file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
colors = ["colorama (>=0.4.6)"]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.4"
|
||||
@@ -636,6 +650,24 @@ files = [
|
||||
[package.dependencies]
|
||||
rapidfuzz = ">=3.8.0,<4.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "loguru"
|
||||
version = "0.7.2"
|
||||
description = "Python logging made (stupidly) simple"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
files = [
|
||||
{file = "loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb"},
|
||||
{file = "loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""}
|
||||
win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""}
|
||||
|
||||
[package.extras]
|
||||
dev = ["Sphinx (==7.2.5)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptiongroup (==1.1.3)", "freezegun (==1.1.0)", "freezegun (==1.2.2)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.4.1)", "mypy (==v1.5.1)", "pre-commit (==3.4.0)", "pytest (==6.1.2)", "pytest (==7.4.0)", "pytest-cov (==2.12.1)", "pytest-cov (==4.1.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.0.0)", "sphinx-autobuild (==2021.3.14)", "sphinx-rtd-theme (==1.3.0)", "tox (==3.27.1)", "tox (==4.11.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "3.0.0"
|
||||
@@ -1031,6 +1063,25 @@ files = [
|
||||
[package.dependencies]
|
||||
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-settings"
|
||||
version = "2.3.2"
|
||||
description = "Settings management using Pydantic"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pydantic_settings-2.3.2-py3-none-any.whl", hash = "sha256:ae06e44349e4c7bff8d57aff415dfd397ae75c217a098d54e9e6990ad7594ac7"},
|
||||
{file = "pydantic_settings-2.3.2.tar.gz", hash = "sha256:05d33003c74c2cd585de97b59eb17b6ed67181bc8a3ce594d74b5d24e4df7323"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pydantic = ">=2.7.0"
|
||||
python-dotenv = ">=0.21.0"
|
||||
|
||||
[package.extras]
|
||||
toml = ["tomli (>=2.0.1)"]
|
||||
yaml = ["pyyaml (>=6.0.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.18.0"
|
||||
@@ -1803,6 +1854,20 @@ files = [
|
||||
{file = "websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "win32-setctime"
|
||||
version = "1.1.0"
|
||||
description = "A small Python utility to set file creation time on Windows"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
files = [
|
||||
{file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"},
|
||||
{file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "yarl"
|
||||
version = "1.9.4"
|
||||
@@ -1909,4 +1974,4 @@ multidict = ">=4.0"
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.11"
|
||||
content-hash = "adfecfda8c14ef2ff44c5640e31d258010cab6bfa6047a5c06ab89c7c1def251"
|
||||
content-hash = "0bfc6b5276e1b3b966d58a4def85071d589b03237c80ff8390af63336f493887"
|
||||
|
||||
@@ -16,7 +16,12 @@ aiohttp = "^3.9.5"
|
||||
bencodepy = "^0.9.5"
|
||||
python-dotenv = "^1.0.1"
|
||||
asyncio = "^3.4.3"
|
||||
pydantic-settings = "^2.3.2"
|
||||
pydantic = "^2.7.3"
|
||||
loguru = "^0.7.2"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
isort = "^5.13.2"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
|
||||
Reference in New Issue
Block a user