mirror of
https://github.com/Viren070/AIOStremio.git
synced 2025-12-01 23:24:19 +01:00
225 lines
7.3 KiB
Python
225 lines
7.3 KiB
Python
import json
|
|
import asyncio
|
|
import httpx
|
|
import os
|
|
import time
|
|
import logging
|
|
import bcrypt
|
|
from collections import defaultdict
|
|
from contextlib import asynccontextmanager
|
|
|
|
import uvicorn
|
|
from cryptography.fernet import Fernet
|
|
from dotenv import load_dotenv
|
|
from fastapi import FastAPI
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.templating import Jinja2Templates
|
|
from pydantic import BaseModel
|
|
|
|
from routes.api import router
|
|
from services.comet import CometService
|
|
from services.easynews import EasynewsService
|
|
from services.mediafusion import MediaFusionService
|
|
from services.torbox import TorboxService
|
|
from services.torrentio import TorrentioService
|
|
from services.debridio import DebridioService
|
|
from services.peerflix import PeerflixService
|
|
from services.watchhub import WatchHubService
|
|
from utils.cache import get_cache_info
|
|
from utils.config import config
|
|
from utils.logger import logger
|
|
|
|
load_dotenv()
|
|
|
|
logging.getLogger("httpx").setLevel(logging.WARNING)
|
|
|
|
# Order is reflected in Stremio
|
|
streaming_services = [
|
|
service
|
|
for service in [
|
|
WatchHubService(),
|
|
TorboxService() if config.debrid_service.lower() == "torbox" else None,
|
|
TorrentioService() if config.debrid_service is not None else None,
|
|
CometService() if config.debrid_service is not None else None,
|
|
MediaFusionService() if os.getenv("MEDIAFUSION_OPTIONS") else None,
|
|
(
|
|
EasynewsService()
|
|
if os.getenv("EASYNEWS_USERNAME") and os.getenv("EASYNEWS_PASSWORD")
|
|
else None
|
|
),
|
|
DebridioService() if config.get_addon_debrid_api_key("debridio") != os.getenv("DEBRID_API_KEY") and config.get_addon_debrid_service("debridio") != config.debrid_service else None,
|
|
PeerflixService() if config.debrid_service is not None else None,
|
|
]
|
|
if service is not None
|
|
]
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
logger.info(f"Cache Info\nSize: {(await get_cache_info())['total_size_mb']}MB")
|
|
yield
|
|
|
|
app = FastAPI(lifespan=lifespan)
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"],
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
USERS_FILE = "db/users.json"
|
|
RATE_LIMIT_MINUTES = 1
|
|
MAX_REQUESTS = 30
|
|
CACHE_TTL = config.cache_ttl_seconds
|
|
|
|
ENCRYPTION_KEY = os.getenv("ENCRYPTION_KEY")
|
|
if not ENCRYPTION_KEY:
|
|
ENCRYPTION_KEY = Fernet.generate_key()
|
|
logger.warning(
|
|
"No ENCRYPTION_KEY set, a new key will be generated on every restart."
|
|
)
|
|
fernet = Fernet(ENCRYPTION_KEY)
|
|
|
|
rate_limits = defaultdict(list)
|
|
|
|
|
|
class User(BaseModel):
|
|
username: str
|
|
password: str
|
|
proxy_streams: bool = True
|
|
|
|
|
|
class RateLimiter:
|
|
def __init__(self, max_requests: int, window_minutes: int):
|
|
self.max_requests = max_requests
|
|
self.window_minutes = window_minutes
|
|
|
|
def is_rate_limited(self, user: str) -> bool:
|
|
now = time.time()
|
|
minute_ago = now - (self.window_minutes * 60)
|
|
|
|
# Clean old requests
|
|
rate_limits[user] = [
|
|
req_time for req_time in rate_limits[user] if req_time > minute_ago
|
|
]
|
|
|
|
# Check if rate limited
|
|
if len(rate_limits[user]) >= self.max_requests:
|
|
logger.info(
|
|
f"Rate limit exceeded for user: {user} ({len(rate_limits[user])}/{self.max_requests})"
|
|
)
|
|
return True
|
|
|
|
# Add new request
|
|
rate_limits[user].append(now)
|
|
return False
|
|
|
|
|
|
rate_limiter = RateLimiter(MAX_REQUESTS, RATE_LIMIT_MINUTES)
|
|
|
|
|
|
class AdminAuth:
|
|
def __init__(self):
|
|
admin_username = os.getenv("ADMIN_USERNAME")
|
|
admin_password = os.getenv("ADMIN_PASSWORD")
|
|
|
|
self.admin_credentials = {
|
|
"username": admin_username,
|
|
"password_hash": bcrypt.hashpw(admin_password.encode(), bcrypt.gensalt()),
|
|
}
|
|
|
|
def verify_admin(self, username: str, password: str) -> bool:
|
|
if username != self.admin_credentials["username"]:
|
|
return False
|
|
return bcrypt.checkpw(password.encode('utf-8'), self.admin_credentials["password_hash"])
|
|
|
|
|
|
admin_auth = AdminAuth()
|
|
|
|
templates = Jinja2Templates(directory="templates")
|
|
|
|
app.include_router(router)
|
|
|
|
|
|
async def sanity_check():
|
|
logger.info("Performing sanity check...")
|
|
|
|
addon_urls = [service.base_url for service in streaming_services]
|
|
|
|
logger.info("Addons | Checking addons...")
|
|
|
|
for url in addon_urls:
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
response = await client.get(url)
|
|
if response.status_code not in [200, 302, 307]:
|
|
logger.warning(f"Addons | ⚠️ {url} (Status: {response.status_code})")
|
|
else:
|
|
logger.info(f"Addons | ✅ {url}")
|
|
except (httpx.ReadTimeout, httpx.ConnectTimeout):
|
|
logger.warning(f"Addons | ⚠️ {url} (Timeout)")
|
|
continue
|
|
except Exception as e:
|
|
logger.warning(f"Addons | ⚠️ {url} ({str(e)})")
|
|
continue
|
|
|
|
logger.info("Config | Checking config...")
|
|
config_path = os.path.join(os.path.dirname(__file__), "data", "config.json")
|
|
with open(config_path, "r") as f:
|
|
example_config = json.load(f)
|
|
|
|
def validate_config_structure(example: dict, current: dict, path: str = ""):
|
|
for key, value in example.items():
|
|
current_path = f"{path}.{key}" if path else key
|
|
if key not in current:
|
|
logger.warning(f"Config | ⚠️ The config is outdated (missing {current_path})")
|
|
exit(1)
|
|
if isinstance(value, dict):
|
|
if not isinstance(current[key], dict):
|
|
logger.warning(f"Config | ⚠️ The config is malformed (expected dict for {current_path} but got {type(current[key])})")
|
|
exit(1)
|
|
validate_config_structure(value, current[key], current_path)
|
|
|
|
validate_config_structure(example_config, config._config)
|
|
|
|
logger.info("Config | ✅ The config is up to date")
|
|
|
|
if (
|
|
not config.debrid_service
|
|
and not os.getenv("MEDIAFUSION_OPTIONS")
|
|
and not (os.getenv("EASYNEWS_USERNAME") and os.getenv("EASYNEWS_PASSWORD"))
|
|
):
|
|
logger.warning("Config | ⚠️ No services configured")
|
|
exit(1)
|
|
|
|
if config.debrid_service and not os.getenv("DEBRID_API_KEY"):
|
|
logger.warning("Config | ⚠️ Default debrid service is configured but no API key is set")
|
|
exit(1)
|
|
|
|
for service_name in config._config.get("addon_config", {}).keys():
|
|
debrid_service = config.get_addon_debrid_service(service_name)
|
|
debrid_api_key = config.get_addon_debrid_api_key(service_name)
|
|
if service_name == "debridio" and debrid_api_key == os.getenv("DEBRID_API_KEY") and debrid_service != config.debrid_service:
|
|
continue
|
|
if debrid_service == config.debrid_service:
|
|
logger.info(f"Config | *️⃣ Using default debrid service ({debrid_service}) for {service_name}")
|
|
else:
|
|
logger.info(f"Config | *️⃣ Using {debrid_service} for {service_name}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(sanity_check())
|
|
uvicorn.run(
|
|
app,
|
|
host="0.0.0.0",
|
|
port=8469,
|
|
log_config={
|
|
"version": 1,
|
|
"disable_existing_loggers": False,
|
|
"loggers": {
|
|
"uvicorn.access": {"level": "WARNING"},
|
|
},
|
|
},
|
|
)
|