import requests import tempfile import os import shutil import subprocess from fastapi import FastAPI, BackgroundTasks from pydantic import BaseModel from threading import Lock import re app = FastAPI(title="Neon Anime Blur & Upload") UPLOAD_URL = "https://litterbox.catbox.moe/resources/internals/api.php" RENDER_UPDATE_ENDPOINT = "https://nt-anime-api.onrender.com/update" HF_AYANO_BASE = "https://a-y-a-n-o-k-o-j-i-dnd-api.hf.space" QUALITIES = ["360p", "720p", "1080p"] queue_lock = Lock() def log(msg: str): print(f"[HF] {msg}", flush=True) class EpisodeExceedsAvailableCount(Exception): pass class StartPayload(BaseModel): anime_id: str anime_name: str def download_video(anime_id: str, episode: int, quality: str) -> str | None: url = f"{HF_AYANO_BASE}/anime/download?id={anime_id}&episode={episode}&quality={quality}" log(f"Fetching link ep {episode} {quality}") headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Referer": "https://animepahe.si/" } try: resp = requests.get(url, headers=headers, timeout=20) resp.raise_for_status() data = resp.json() if data.get("status") == 422: raise EpisodeExceedsAvailableCount() if data.get("status") != 200: return None video_url = data["direct_link"] log(f"Got direct link ep {episode} {quality}") except EpisodeExceedsAvailableCount: raise except Exception as e: log(f"Link error ep {episode} {quality}: {e}") return None tmp_path = tempfile.mktemp(suffix=".mp4") log(f"Downloading ep {episode} {quality}") try: with requests.get(video_url, headers=headers, stream=True, timeout=120) as r: r.raise_for_status() with open(tmp_path, "wb") as f: shutil.copyfileobj(r.raw, f) log(f"Downloaded ep {episode} {quality}") return tmp_path except Exception as e: log(f"Download failed ep {episode} {quality}: {e}") if os.path.exists(tmp_path): os.remove(tmp_path) return None def get_filename(anime_name: str, ep: int, quality: str) -> str: slug = re.sub(r'[^a-z0-9-]+', '-', anime_name.lower()).strip('-') return f"nt-animes_{slug}_ep{ep}_{quality}.mp4" def upload_to_litterbox(file_path: str, file_name: str) -> str: log(f"Uploading {file_name}") try: with open(file_path, "rb") as f: files = {"fileToUpload": (file_name, f)} data = {"reqtype": "fileupload", "time": "72h"} r = requests.post(UPLOAD_URL, data=data, files=files, timeout=180) r.raise_for_status() url = r.text.strip() log(f"Uploaded: {url}") return url except Exception as e: log(f"Upload failed: {e}") raise def notify_render(anime_id: str, episode: int, quality: str, file_url: str, file_name: str, status: int): payload = { "anime_id": anime_id, "episode": episode, "quality": quality, "file_url": file_url, "file_name": file_name, "status": status } log(f"Notifying Render status={status} ep={episode} {quality}") try: requests.post(RENDER_UPDATE_ENDPOINT, json=payload, timeout=20) except Exception as e: log(f"Notify failed: {e}") def blur_video(input_path: str) -> str: output = tempfile.mktemp(suffix="_blurred.mp4") filter_graph = ( "[0:v]crop=74:17:0:0,boxblur=luma_radius=4:luma_power=1:chroma_radius=0[top];" "[0:v][top]overlay=0:0," "drawtext=text='nt-animes':x=8:y=8:fontcolor=white:fontsize=12" ) cmd = [ "ffmpeg", "-y", "-i", input_path, "-filter_complex", filter_graph, "-c:v", "libx264", "-preset", "fast", "-crf", "23", "-c:a", "copy", output ] log("Blurring video") try: subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) log("Blur complete") return output except Exception as e: log(f"Blur failed: {e}") raise def process_anime(anime_id: str, anime_name: str): log(f"Started processing {anime_id} - {anime_name}") episode = 1 while True: processed = False for quality in QUALITIES: try: local_file = download_video(anime_id, episode, quality) if not local_file: continue blurred_file = blur_video(local_file) os.remove(local_file) file_name = get_filename(anime_name, episode, quality) file_url = upload_to_litterbox(blurred_file, file_name) os.remove(blurred_file) notify_render(anime_id, episode, quality, file_url, file_name, 2) processed = True except EpisodeExceedsAvailableCount: log("All episodes processed") notify_render(anime_id, 0, "", "", "", 5) return except Exception as e: log(f"Error ep {episode} {quality}: {e}") notify_render(anime_id, episode, quality, "", "", 3) if not processed: log("No more episodes") notify_render(anime_id, 0, "", "", "", 5) break episode += 1 log("Processing finished") @app.post("/start") def start(payload: StartPayload, bg: BackgroundTasks): log(f"Queueing {payload.anime_id} - {payload.anime_name}") with queue_lock: bg.add_task(process_anime, payload.anime_id, payload.anime_name) return {"code": 4, "message": "Queued"}