| 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"} |