Merge pull request #1 from dreulavelle/spoked

Spoked
This commit is contained in:
Goldy
2024-06-12 02:11:36 +02:00
committed by GitHub
7 changed files with 445 additions and 215 deletions
+2
View File
@@ -2,6 +2,8 @@
__pycache__/
*.py[cod]
*$py.class
logs/
.vscode/
# C extensions
*.so
+68
View File
@@ -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
View File
@@ -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}"
+43
View File
@@ -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()
+13
View File
@@ -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
View File
@@ -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"
+5
View File
@@ -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"]