270 lines
9.5 KiB
Python
270 lines
9.5 KiB
Python
from fastapi import FastAPI, Request, Form, BackgroundTasks
|
|
from fastapi.responses import HTMLResponse, JSONResponse
|
|
from fastapi.templating import Jinja2Templates
|
|
from fastapi.staticfiles import StaticFiles
|
|
from functools import partial
|
|
import json, download, asyncio
|
|
from typing import Optional
|
|
import logging, os
|
|
from logging.handlers import TimedRotatingFileHandler
|
|
|
|
# Ensure log directory exists
|
|
os.makedirs("/data/logs", exist_ok=True)
|
|
|
|
# Setup timed rotating logger
|
|
# log_path = "/data/logs/syllabus.log"
|
|
logger = logging.getLogger("syllabus")
|
|
logger.setLevel(logging.DEBUG)
|
|
|
|
# Remove any default handlers
|
|
logger.handlers = []
|
|
|
|
# Set up TimedRotatingFileHandler
|
|
handler = TimedRotatingFileHandler(
|
|
filename="/data/logs/syllabus.log",
|
|
when="midnight", # Rotate at midnight
|
|
interval=30, # Every 30 day
|
|
backupCount=12, # Keep last 7 logs
|
|
encoding="utf-8",
|
|
utc=False # Use UTC for time reference
|
|
)
|
|
|
|
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
|
|
handler.setFormatter(formatter)
|
|
logger.addHandler(handler)
|
|
|
|
# App setup
|
|
app = FastAPI()
|
|
app.mount("/data", StaticFiles(directory="/data"), name="data")
|
|
templates = Jinja2Templates(directory="templates")
|
|
loop = asyncio.get_event_loop()
|
|
|
|
# Optional cache
|
|
cached_data = None
|
|
|
|
|
|
|
|
# Middleware
|
|
@app.middleware("http")
|
|
async def log_requests(request: Request, call_next):
|
|
try:
|
|
response = await call_next(request)
|
|
except Exception as e:
|
|
logger.exception(f"EXCEPTION: {request.method} {request.url} - {str(e)}")
|
|
return JSONResponse(
|
|
status_code=500,
|
|
content={"detail": "Internal Server Error"},
|
|
)
|
|
|
|
logger.info(
|
|
f"request_client={request.client.host}:{request.client.port}, "
|
|
f"request_method={request.method}, request_url={request.url}, "
|
|
f"status_code={response.status_code}"
|
|
)
|
|
return response
|
|
|
|
|
|
|
|
|
|
|
|
# api
|
|
|
|
# @app.post("/ebook/download", description="Download an ebook via a url.")
|
|
# async def ebookDownload(
|
|
# background_tasks: BackgroundTasks,
|
|
# url: str = Form(...),
|
|
# author: str = Form(...)
|
|
# ):
|
|
# try:
|
|
# background_tasks.add_task(download.ebook,url,author)
|
|
# # download.dropout.show(show,season,episode)
|
|
# return JSONResponse(status_code=200, content={"status": "success", "message": "Book downloaded."})
|
|
# except Exception as e:
|
|
# return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})
|
|
|
|
@app.get("/dropout/update")
|
|
async def dropoutUpdate(force: bool = False):
|
|
global cached_data
|
|
try:
|
|
download.dropout.series(force)
|
|
with open('/data/dropout.json') as f:
|
|
cached_data = json.load(f)
|
|
return JSONResponse(status_code=200, content={"status": "success", "message": "Series grab complete."})
|
|
except Exception as e:
|
|
return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})
|
|
|
|
@app.get("/dropout/series")
|
|
async def dropoutSeries():
|
|
global cached_data
|
|
if cached_data is None:
|
|
await dropoutUpdate()
|
|
try:
|
|
return JSONResponse(content=cached_data)
|
|
except:
|
|
return JSONResponse(content={"error": "File not found"}, status_code=404)
|
|
|
|
async def get_show_data(show: str, force: bool = False):
|
|
global cached_data
|
|
if cached_data is None:
|
|
await dropoutUpdate()
|
|
|
|
for item in cached_data:
|
|
if show == item["SHOW"] or show == item["LINK"]:
|
|
if "SEASONS" not in item or force is not False:
|
|
item['SEASONS'] = download.grab.season(item['URL'])
|
|
return item
|
|
return None
|
|
|
|
def get_latest_season(item):
|
|
seasons = item.get("SEASONS")
|
|
if seasons and isinstance(seasons, list):
|
|
try:
|
|
numeric_seasons = [int(s) for s in seasons if str(s).isdigit()]
|
|
if numeric_seasons:
|
|
return max(numeric_seasons)
|
|
except Exception as e:
|
|
logging.error(f"Error getting latest season: {e}")
|
|
return None
|
|
|
|
@app.post("/dropout/custom", description="")
|
|
async def dropout_download(
|
|
background_tasks: BackgroundTasks,
|
|
url: str = Form(...),
|
|
directory: str = Form(...),
|
|
prefix: Optional[str] = Form(None)
|
|
):
|
|
# Ensure output directory exists
|
|
os.makedirs(directory, exist_ok=True)
|
|
|
|
try:
|
|
background_tasks.add_task(download.dropout.custom, url, directory, prefix)
|
|
return {"status": "success", "message": "Download started"}
|
|
except Exception as e:
|
|
raise JSONResponse(status_code=500, content=f"Download failed: {str(e)}")
|
|
|
|
@app.post("/dropout/download", description="Download an entire season from episode 1. Ignores behind the scenes and trailers.")
|
|
async def dropout_download(
|
|
background_tasks: BackgroundTasks,
|
|
show: str = Form(...),
|
|
season: Optional[int] = Form(None),
|
|
latest: bool = Form(True),
|
|
archive: bool = Form(False),
|
|
specials: bool = Form(False),
|
|
episode_start: Optional[int] = Form(None)
|
|
):
|
|
try:
|
|
# Resolve latest season if requested
|
|
if latest and season is None:
|
|
show_data = await get_show_data(show, True)
|
|
if not show_data:
|
|
return JSONResponse(
|
|
status_code=404,
|
|
content={"status": "error", "message": "Show not found"}
|
|
)
|
|
|
|
season = get_latest_season(show_data)
|
|
if season is None:
|
|
return JSONResponse(
|
|
status_code=400,
|
|
content={"status": "error", "message": "No valid seasons found"}
|
|
)
|
|
|
|
# Ensure season is specified by now
|
|
if season is None:
|
|
return JSONResponse(
|
|
status_code=400,
|
|
content={"status": "error", "message": "Season is required unless 'latest' is used."}
|
|
)
|
|
|
|
task_msg = f"{'Adding to archive' if archive else 'Starting download'} for show '{show}', season {season}{' specials' if specials else ''}."
|
|
logger.info(f"message={task_msg}")
|
|
|
|
# Schedule the background task
|
|
if archive:
|
|
background_tasks.add_task(download.dropout.archive, show, season)
|
|
else:
|
|
background_tasks.add_task(download.dropout.show, show, season, specials, episode_start)
|
|
|
|
return JSONResponse(
|
|
status_code=200,
|
|
content={
|
|
"status": "success",
|
|
"message": (task_msg)
|
|
}
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.exception(f"Unhandled exception during /dropout/download: {e}")
|
|
return JSONResponse(
|
|
status_code=500,
|
|
content={"status": "error", "message": "An unexpected error occurred."}
|
|
)
|
|
|
|
# @app.post("/dropout/download/specials", description="Downloads a seasons behind the scenes and trailers, ignores main episodes.")
|
|
# async def dropoutDownload(
|
|
# background_tasks: BackgroundTasks,
|
|
# show: str = Form(...),
|
|
# season: int = Form(...),
|
|
# episode: Optional[int] = Form(None)
|
|
# ):
|
|
# try:
|
|
# logger.info(f'message=Received download request for specials of season {season} of {show}.')
|
|
# background_tasks.add_task(download.dropout.specials,show,season,episode)
|
|
# # download.dropout.show(show,season,episode)
|
|
# return JSONResponse(status_code=200, content={"status": "success", "message": "Series downloaded."})
|
|
# except Exception as e:
|
|
# return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})
|
|
|
|
@app.post("/ydl")
|
|
async def ydl(background_tasks: BackgroundTasks, url: str = Form(...), location: str = Form(...)):
|
|
try:
|
|
background_tasks.add_task(download.youtube.ydl, url, location)
|
|
# download.youtube.ydl(url,location)
|
|
# grab.thumbnail(ydl,url,location)
|
|
return JSONResponse(status_code=200, content={"status": "success", "message": "Video download completed."})
|
|
except Exception as e:
|
|
return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})
|
|
|
|
|
|
|
|
|
|
|
|
#web ui
|
|
@app.get("/", include_in_schema=False, response_class=HTMLResponse)
|
|
async def index(request: Request):
|
|
global cached_data
|
|
try:
|
|
if cached_data is None:
|
|
await dropoutUpdate()
|
|
return templates.TemplateResponse("index.html", {"request": request, "data": cached_data})
|
|
except Exception as e:
|
|
return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})
|
|
|
|
@app.get("/show/{show}", include_in_schema=False, response_class=HTMLResponse)
|
|
async def index(request: Request, show: str):
|
|
try:
|
|
item = await get_show_data(show)
|
|
if item:
|
|
return templates.TemplateResponse("show.html", {"request": request, "show": item})
|
|
else:
|
|
return JSONResponse(status_code=404, content={"status": "error", "message": "Show not found"})
|
|
except Exception as e:
|
|
return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})
|
|
|
|
@app.get("/ydl", include_in_schema=False)
|
|
async def webpage(request: Request):
|
|
try:
|
|
return templates.TemplateResponse("ydl.html", {"request": request})
|
|
except Exception as e:
|
|
return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})
|
|
|
|
@app.get("/dropout", include_in_schema=False)
|
|
async def webpage(request: Request):
|
|
global cached_data
|
|
if cached_data is None:
|
|
await dropoutUpdate()
|
|
try:
|
|
return templates.TemplateResponse("dropout.html", {"request": request, "data": cached_data})
|
|
except Exception as e:
|
|
return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})
|