#!/usr/bin/env -S uv run --script # /// script # dependencies = ["psycopg[binary]", "click", "httpx"] # /// import argparse import contextlib import dataclasses import datetime import hashlib import itertools import json import logging import os import re import shlex import shutil import socket import subprocess import threading import time import typing from functools import partial, wraps from os import getenv from pathlib import Path from typing import Any, Callable from urllib.parse import parse_qs from wsgiref.simple_server import WSGIRequestHandler, make_server import click import httpx import psycopg from psycopg import sql as pgsql logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s: %(message)s") HandlerFunc = Callable[["Request"], "Response"] @dataclasses.dataclass class FfprobeResult: duration_sec: float duration_human: str codec: str fps: float size_bytes: int width: int height: int bitrate: int container: str ar: float sample_ar: float tags: dict[str, str] @property def resolution(self) -> str: return f"{self.width}x{self.height}" def ffprobe(video_path: Path) -> FfprobeResult: proc = subprocess.run( args=[ "ffprobe", "-v", "error", "-show_streams", "-show_format", "-print_format", "json", str(video_path), ], text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) try: proc.check_returncode() except subprocess.CalledProcessError as e: logging.error(f"failed to run ffprobe. stderr={e.stderr}") raise output: dict = json.loads(proc.stdout) video_stream: dict = [ s for s in output.get("streams", []) if s.get("codec_type") == "video" ][0] video_format: dict = output.get("format", {}) width, height = video_stream.get("width"), video_stream.get("height") codec = video_stream.get("codec_name") duration = float(video_format.get("duration", 0)) fps_long = eval(video_stream.get("r_frame_rate", "0")) fps = float(f"{fps_long:.3f}") if fps_long else 0.0 size = int(video_format.get("size", 0)) duration_time = datetime.timedelta(seconds=int(duration)) bitrate = int(video_stream.get("bit_rate", video_format.get("bit_rate", 0))) tags = video_format.get("tags", {}) try: sample_w, sample_h = list( map(int, video_stream["sample_aspect_ratio"].split(":")) ) sample_ar = (sample_w / sample_h) or 1.0 except: sample_ar = 1.0 return FfprobeResult( duration_sec=duration, duration_human=str(duration_time), codec=codec, fps=fps, size_bytes=size, width=width, bitrate=bitrate, height=height, container=video_path.suffix.lstrip(".").lower(), ar=width / height if height else 1.0, sample_ar=sample_ar, tags=tags, ) def hash_partial(f: Path) -> str: sha1 = hashlib.sha1() chunk_size = 1024 * 1024 * 10 # 10MB chunk size total_read = 0 with f.open("rb") as file: while chunk := file.read(chunk_size): total_read += len(chunk) sha1.update(chunk) break # Only reads the first 10MB return f"sha1:{total_read}:{sha1.hexdigest()}" def chunked(iterable, n): it = iter(iterable) while True: chunk = tuple(itertools.islice(it, n)) if not chunk: return yield chunk def retry(max_attempts: int = 3, delay: float = 1.0): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): last_exception = None for attempt in range(1, max_attempts + 1): try: return func(*args, **kwargs) except Exception as e: last_exception = e if attempt < max_attempts: logging.warning( f"Attempt {attempt} failed: {e}. Retrying in {delay}s..." ) time.sleep(delay) else: logging.error(f"All {max_attempts} attempts failed.") raise last_exception return wrapper return decorator def video_exists(con: psycopg.Connection, file_path: str) -> bool: stmt = con.execute( "UPDATE videos SET last_seen_at = NOW() WHERE file_path = %s RETURNING true AS exists", [file_path], ) row = stmt.fetchone() if not row: return False return row["exists"] def save_video( con: psycopg.Connection, category: str, file_path: str, ffprobe_data: dict, video_hash: str, ): sql = """ INSERT INTO videos(category, file_path, ffprobe, hash_partial) VALUES (%s, %s, %s, %s) ON CONFLICT(file_path) DO UPDATE SET last_seen_at = NOW();""" con.execute( sql, [ category, file_path, json.dumps(ffprobe_data), video_hash, ], ) def save_error(con: psycopg.Connection, file_path: str, stderr: str): sql = """INSERT INTO scan_state(file_path, stderr) VALUES (%s, %s) ON conflict do nothing""" con.execute( sql, [ file_path, stderr, ], ) def category_for_file(file_path: Path) -> str: s = str(file_path) if s.startswith("/Volumes/"): return f"downloaded.{file_path.parts[1].lower()}" if s.startswith("/mnt/box"): return "box" return "local" def scan_videos(con: psycopg.Connection, video_paths: list[Path]) -> None: new_videos = [] for i, f in enumerate(video_paths): if "/_picks/" in str(f): continue progress = f"[{i + 1}/{len(video_paths)}]" if f.is_dir(): # Recursively find files in directory scan_videos(con, list(f.glob("**/*"))) continue if f.suffix.lower() not in [".mp4", ".mkv", ".avi", ".mov", ".wmv", ".webm"]: continue file_path = str(f.absolute()) if video_exists(con, file_path=file_path): logging.debug(f"{progress} video already exists {f=}") continue logging.info(f"{progress} probing {f=}") try: probed = ffprobe(f) except Exception as e: logging.error(f"Failed to probe {f}: {e}") save_error(con, file_path=file_path, stderr=str(e)) continue file_hash = hash_partial(f) save_video( category=category_for_file(f), con=con, file_path=file_path, ffprobe_data=probed.__dict__, video_hash=file_hash, ) logging.info(f"{progress} saved: {f.name}") new_videos.append(file_path) con.commit() if new_videos: parse_releases(con, new_videos) def parse_releases(con: psycopg.Connection, file_paths: list[str]) -> None: api_key = os.getenv("OPENROUTER_API_KEY") if not api_key: logging.warning("OPENROUTER_API_KEY not set, skipping filename parsing") return # Chunk file_paths by 20 items to avoid prompt token limits for i, chunk in enumerate(chunked(file_paths, 20), 1): filenames = [Path(p).name for p in chunk] logging.info( f"Parsing filenames for {len(filenames)} videos (chunk {i}) via OpenRouter..." ) try: parsed_data = parse_filenames_with_ai(filenames, api_key) for item in parsed_data: actors = item.get("actors") if not isinstance(actors, list) or len(actors) == 0: continue fname = item.get("filename") if not fname: continue # Find the full path that matches this filename in the current chunk full_path = next((p for p in chunk if Path(p).name == fname), None) if not full_path: continue # Update the release column release_info = {k: v for k, v in item.items() if k != "filename"} con.execute( "UPDATE videos SET release = %s WHERE file_path = %s", [json.dumps(release_info), full_path], ) con.commit() logging.info(f"Successfully updated release info for chunk {i}") except Exception as e: logging.error( f"Failed to parse filenames or update database for chunk {i}: {e}" ) @retry(max_attempts=3, delay=0.1) def parse_filenames_with_ai(filenames: list[str], api_key: str) -> list[dict]: system_prompt = """you are an expert in parsing file names. your task is to parse the actors, studio and date, title out of filenames give me a JSON array with these fields [{filename, studio, released_at, title, actors: [...]}}]. Omit the missing / empty fields. Actor names are usually 2 words (name and last name) but sometimes they only contain a single word. Ignore male names. Title is the remaining part after studio, date, actors; and don't usually contain the actor names. Ignore the quality and category indicators. output only valid JSON without any wrappers or quotes. for example: InTheCrack.E1890.Casey.Norhman.Provence.XXX.1080p.HEVC.x265.PRT.mp4 -> studio=InTheCrack, actors=["Casey Norhman"], title=E1890 BlackedRaw.26.05.16.Agatha.Vega.And.Ella.Hughes.Knockout.Babes.Fuck.Two.Cops.On.Duty.XXX.1080p.HEVC.x265.PRT.torrent -> {studio=BlackedRaw, actors=["Agatha Vega", "Ella Hughes"], title="Knockout Babes Fuck Two Cops On Duty", released_at=2026-05-16} StepSiblingsCaught.26.05.14.Nata.Gold.XXX.720p.HEVC.x265.PRT.mp4 -> studio=StepSiblingsCaught, released_at=2026-05-14, actors=["Nata Gold"] HookupHotshot.26.02.06.Episode.453.Shrooms.Q.XXX.720p.HEVC.x265.PRT.mp4 -> studio=HookupHotshot, released_at=2026-02-06, actors=["Shrooms Q"], title="Episode 453" """ user_prompt = "\n".join(filenames) response = httpx.post( "https://openrouter.ai/api/v1/chat/completions", headers={ "Authorization": f"Bearer {api_key}", }, json={ "model": "mistralai/ministral-3b-2512", "prompt_cache_key": "file_parsing", "messages": [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, ], "temperature": 0, }, timeout=20, ) response.raise_for_status() result = response.json() content = result["choices"][0]["message"]["content"].strip() # Remove potential markdown code blocks if content.startswith("```"): content = re.sub(r"^```(?:json)?\s*|\s*```$", "", content, flags=re.MULTILINE) parsed_data = json.loads(content) if not isinstance(parsed_data, list): parsed_data = [parsed_data] # Clean output: omit empty strings and empty arrays cleaned_data = [] for entry in parsed_data: cleaned_entry = { k: v for k, v in entry.items() if v != "" and not (isinstance(v, list) and not v) } actors = cleaned_entry.get("actors", []) if isinstance(actors, list) and len(actors) > 0: actors = [a for a in actors if a and "@" not in a] cleaned_entry["actors"] = actors cleaned_data.append(cleaned_entry) return cleaned_data @dataclasses.dataclass @dataclasses.dataclass class Request: path: str method: str payload: dict[str, Any] query: dict[str, str] environ: dict[str, Any] @classmethod def from_environ(cls, environ: dict) -> "Request": method = environ["REQUEST_METHOD"].upper() query = parse_qs(environ.get("QUERY_STRING", ""), keep_blank_values=True) payload = {} content_type = environ.get("CONTENT_TYPE", "") if method == "POST" and "application/json" in content_type: try: length = int(environ.get("CONTENT_LENGTH", 0)) if length > 0: payload = json.loads(environ["wsgi.input"].read(length)) except (ValueError, json.JSONDecodeError): pass return cls( path=environ["PATH_INFO"], method=method, payload=payload, query={k: v[0] for k, v in query.items()}, environ=environ, ) @dataclasses.dataclass class Response: status: str headers: list[tuple[str, str]] body: Any @classmethod def error(cls, message: str) -> "Response": return cls( status="400 Bad Request", headers=[("Content-Type", "application/json")], body=json.dumps({"error": message}) + "\n", ) @classmethod def from_html(cls, html: str) -> "Response": return cls( status="200 OK", headers=[("Content-Type", "text/html; charset=utf-8")], body=html, ) @classmethod def from_json(cls, data: Any) -> "Response": def _default(obj): if isinstance(obj, (datetime.date, datetime.datetime)): return obj.isoformat() raise TypeError(f"Object of type {type(obj)} is not JSON serializable") return cls( status="200 OK", headers=[("Content-Type", "application/json")], body=json.dumps(data, default=_default) + "\n", ) @classmethod def from_exception(cls, e: Exception) -> "Response": return cls( status="500 Internal Server Error", headers=[("Content-Type", "application/json")], body=json.dumps({"error": str(e)}) + "\n", ) class TinyAPI: def __init__(self, handlers: dict[str, HandlerFunc]): self.routes = self._prepare_routes(handlers) def _prepare_routes(self, handlers: dict[str, HandlerFunc]): routes = [] for k, h in handlers.items(): parts = k.split(maxsplit=1) method = parts[0].upper() path = parts[1].rstrip("/") or "/" try: routes.append((method, re.compile(f"^{path}$"), h)) except re.error as e: raise ValueError(f"Invalid regex pattern '{path}': {e}") return routes @staticmethod def find_free_port() -> int: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.bind(("", 0)) return s.getsockname()[1] def __call__(self, environ: dict, start_response: Callable): req = Request.from_environ(environ) path = req.path.rstrip("/") or "/" handler = None for method, pattern, h in self.routes: if req.method == method and pattern.match(path): handler = h break if not handler: res = Response.from_json({"error": "Not Found"}) res.status = "404 Not Found" else: try: res = handler(req) except Exception as e: logging.exception("Handler crash") res = Response.from_exception(e) body = res.body if isinstance(res.body, bytes) else res.body.encode("utf-8") headers = res.headers + [("Content-Length", str(len(body)))] start_response(res.status, headers) return [body] @contextlib.contextmanager def serve(self, host: str = "localhost", port: int = 0): if port == 0: port = self.find_free_port() class LoggedRequestHandler(WSGIRequestHandler): def log_message(self, format: str, *args: Any) -> None: # args usually contains (request_line, status_code, size) # We redirect to our configured logger instead of sys.stderr logging.info("%s - %s", self.address_string(), format % args) server = make_server(host, port, self, handler_class=LoggedRequestHandler) url = f"http://{host}:{port}" thread = threading.Thread(target=server.serve_forever, daemon=True) thread.start() logging.info(f"Serving on {url}") try: yield url finally: logging.info("Shutting down server...") server.shutdown() server.server_close() thread.join(timeout=5) @contextlib.contextmanager def connect_db() -> typing.Generator[psycopg.Connection, typing.Any, typing.Any]: with psycopg.connect( "postgres://abdus:abdus@db.abdus.dev:5444/snot?sslmode=disable" ) as conn: conn.row_factory = psycopg.rows.dict_row yield conn def handle_search(req: Request, conn: psycopg.Connection) -> Response: query: str = req.payload.get("query", "") if not query: return Response.error("Query is empty") where_sql, where_params = filter_to_where(query) query_stmt = pgsql.SQL( """ with q as (select id, filename_from_path(file_path) as file_name, file_path, actors, size_bytes / 1048576 as size_mb, (ffprobe->>'width') || 'x' || (ffprobe->>'height') as resolution, duration_human, created_at from videos) select * from q where {filter} order by created_at desc """ ).format(filter=pgsql.SQL(where_sql)) with conn.cursor() as cursor: rows = cursor.execute(query_stmt, where_params).fetchall() return Response.from_json( [ { **row, "download_url": "https://u201686:T6672ICVoWAedECH@u201686.your-storagebox.de/files" + row["file_path"].replace("/mnt/box/files", ""), } for row in rows ] ) def tokenize_filter_groups(filter_query: str) -> list[list[str]]: lexer = shlex.shlex(filter_query, posix=True, punctuation_chars="|") lexer.commenters = "" lexer.whitespace_split = True tokens = list(lexer) groups: list[list[str]] = [[]] for token in tokens: if token == "|": if groups[-1]: groups.append([]) continue groups[-1].append(token) return [group for group in groups if group] def token_to_condition(token: str) -> tuple[str | None, list[typing.Any]]: if ":" in token: key, value = token.split(":", 1) key = key.strip().lower() value = value.strip() if not value: return None, [] if key == "file_name": return "file_name ILIKE %s", [f"%{value}%"] if key == "size_mb": match = re.match( r"^(>=|<=|>|<|=)?\s*(\d+(?:\.\d+)?)$", value, re.IGNORECASE ) if not match: return None, [] op = match.group(1) or "=" num = float(match.group(2)) return f"size_mb {op} %s", [num] if key == "actor": return "actors @> %s::text[]", [[value]] return None, [] return "file_name ILIKE %s", [f"%{token}%"] def filter_to_where(filter_query: str) -> tuple[str, list[typing.Any]]: groups = tokenize_filter_groups(filter_query=filter_query) if not groups: return "TRUE", [] or_clauses: list[str] = [] params: list[typing.Any] = [] for group in groups: and_clauses: list[str] = [] for token in group: clause, clause_params = token_to_condition(token=token) if not clause: continue and_clauses.append(clause) params.extend(clause_params) if and_clauses: or_clauses.append("(" + " AND ".join(and_clauses) + ")") if not or_clauses: return "TRUE", [] return " OR ".join(or_clauses), params def handle_home(req: Request, query: str = "") -> Response: template_file = Path(__file__).parent / "snot.html" html = template_file.read_text() if query: injected_json = json.dumps({"query": query}) html = f"\n" + html return Response.from_html(html) def handle_assets(req: Request, base_path: Path) -> Response: asset_path = base_path / req.path.lstrip("/") if not asset_path.exists() or not asset_path.is_file(): return Response( status="404 Not Found", headers=[("Content-Type", "text/plain")], body="Asset not found\n", ) content_type = "text/plain" if asset_path.suffix == ".js": content_type = "application/javascript" elif asset_path.suffix == ".css": content_type = "text/css" elif asset_path.suffix in [".html", ".htm"]: content_type = "text/html" elif asset_path.suffix == ".json": content_type = "application/json" return Response( status="200 OK", headers=[("Content-Type", content_type)], body=asset_path.read_text(), ) @contextlib.contextmanager def run_htmlpopup(address: str): exe_path = shutil.which("htmlpopup") if not exe_path: raise RuntimeError( "htmlpopup executable not found in PATH. Please install it to use the HTML popup feature." ) proc = subprocess.Popen( [exe_path, "--title", "s·m·u·t", address], text=True, stdout=subprocess.PIPE ) try: yield proc.wait() finally: proc.terminate() @click.group(invoke_without_command=True) @click.pass_context def cli(ctx: click.Context): """Snot - Video Browser and Scanner""" if ctx.invoked_subcommand is None: ctx.invoke(serve) @cli.command() @click.option( "--port", type=int, default=int(getenv("PORT", "0")), help="Port to run the server on", ) @click.option("--query", type=str, default="", help="Initial query string") def serve(port: int, query: str): """Start the web UI""" with connect_db() as conn: handlers = { "POST /search": partial(handle_search, conn=conn), "GET /": partial(handle_home, query=query), "GET /.*": partial(handle_assets, base_path=Path(__file__).parent), } api = TinyAPI(handlers=handlers) with api.serve(port=port) as server_url: logging.info(f"Server running at {server_url}") logging.info("Available endpoints:") for path in handlers.keys(): logging.info(f" {path}") with run_htmlpopup(server_url): logging.info("HTML popup started. Press Ctrl+C to stop.") @cli.command() @click.argument("paths", nargs=-1, type=Path) def scan(paths: list[Path]): """Scan video files and update the database""" if not paths: click.echo("No paths provided to scan.") return with connect_db() as con: scan_videos(con=con, video_paths=list(paths)) @cli.command("mark-deletion") @click.argument("paths", nargs=-1, type=Path) def mark_deletion(paths: list[Path]): """Mark video files for deletion by setting marked_for_deletion_at""" if not paths: click.echo("No paths provided.") return with connect_db() as con: for path in paths: file_name = path.name rows = con.execute( "UPDATE videos SET marked_for_deletion_at = NOW() WHERE file_name = %s RETURNING file_path", [file_name], ).fetchall() if rows: for row in rows: click.echo(f"Marked for deletion: {row['file_path']}") else: click.echo(f"Not found in database: {file_name}", err=True) continue if not click.get_text_stream("stdin").isatty(): continue stem = path.stem siblings = con.execute( "SELECT file_path, file_name FROM videos WHERE file_name != %s AND file_name LIKE %s AND marked_for_deletion_at IS NULL", [file_name, f"{stem}.%"], ).fetchall() for sibling in siblings: if click.confirm(f" Also mark {sibling['file_name']}?", default=False): con.execute( "UPDATE videos SET marked_for_deletion_at = NOW() WHERE file_path = %s", [sibling["file_path"]], ) click.echo(f" Marked for deletion: {sibling['file_path']}") con.commit() if __name__ == "__main__": cli()