"""Бэкенд vv_ext: Flask + SQLite. Всего 6 ручек, без авторизации.""" from __future__ import annotations import os import re from datetime import date from urllib.parse import quote, unquote from flask import Flask, jsonify, request, send_from_directory from db import connect, init_db VIDEO_DIR = os.environ.get("VIDEO_DIR", os.path.join(os.path.dirname(__file__), "videos")) VIDEO_URL_PREFIX = os.environ.get("VIDEO_URL_PREFIX", "/videos/") FRONTEND_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "frontend") ALLOWED_EXT = {".mp4", ".webm", ".mov", ".m4v"} TAG_RE = re.compile(r"[0-9a-zа-яё_\-]{1,32}") app = Flask(__name__) def normalize_tag(raw: str) -> str: tag = raw.strip().lower().replace(" ", "_").replace("#", "") return tag if TAG_RE.fullmatch(tag) else "" def video_url(filename: str) -> str: return VIDEO_URL_PREFIX + quote(filename) @app.get("/api/health") def health(): return jsonify(ok=True) @app.get("/api/tags") def list_tags(): with connect() as con: rows = con.execute( "SELECT t.name, COUNT(vt.filename) AS count FROM tags t " "JOIN video_tags vt ON vt.tag_id = t.id " "GROUP BY t.id ORDER BY count DESC, t.name LIMIT 200", ).fetchall() return jsonify([{"name": r["name"], "count": r["count"]} for r in rows]) @app.get("/api/videos") def list_videos(): tag = normalize_tag(request.args.get("tag", "")) q = request.args.get("q", "").strip() sort = request.args.get("sort", "new") page = max(1, int(request.args.get("page", 1))) per_page = min(60, max(1, int(request.args.get("per_page", 20)))) offset = (page - 1) * per_page where: list[str] = [] params: list = [] if tag: where.append( "v.filename IN (SELECT vt.filename FROM video_tags vt " "JOIN tags t ON t.id = vt.tag_id WHERE t.name = ?)" ) params.append(tag) if q: where.append("v.filename LIKE ?") params.append(f"%{q}%") where_sql = f"WHERE {' AND '.join(where)}" if where else "" order_sql = "v.likes DESC, v.filename" if sort == "likes" else "v.filename DESC" with connect() as con: total = con.execute(f"SELECT COUNT(*) FROM videos v {where_sql}", params).fetchone()[0] rows = con.execute( f"SELECT v.filename, v.likes FROM videos v {where_sql} " f"ORDER BY {order_sql} LIMIT ? OFFSET ?", (*params, per_page, offset), ).fetchall() fnames = [r["filename"] for r in rows] tags_map: dict[str, list[str]] = {f: [] for f in fnames} if fnames: ph = ",".join("?" * len(fnames)) for r in con.execute( "SELECT vt.filename, t.name FROM video_tags vt " f"JOIN tags t ON t.id = vt.tag_id WHERE vt.filename IN ({ph})", fnames, ): tags_map[r["filename"]].append(r["name"]) items = [ {"file": r["filename"], "url": video_url(r["filename"]), "likes": r["likes"], "tags": sorted(tags_map[r["filename"]])} for r in rows ] pages = (total + per_page - 1) // per_page return jsonify({"items": items, "page": page, "pages": pages, "total": total}) @app.post("/api/videos//like") def like_video(raw_file: str): filename = unquote(raw_file) fwd = request.headers.get("X-Forwarded-For", request.remote_addr or "?") ip = fwd.split(",")[0].strip() today = date.today().isoformat() with connect() as con: row = con.execute("SELECT likes FROM videos WHERE filename = ?", (filename,)).fetchone() if row is None: return jsonify(error="not found"), 404 cur = con.execute( "INSERT OR IGNORE INTO likes_log(ip, filename, day) VALUES (?, ?, ?)", (ip, filename, today), ) if cur.rowcount == 0: return jsonify(likes=row["likes"], already=True) con.execute("UPDATE videos SET likes = likes + 1 WHERE filename = ?", (filename,)) likes = row["likes"] + 1 return jsonify(likes=likes, already=False) @app.post("/api/videos//tags") def add_tag(raw_file: str): filename = unquote(raw_file) tag = normalize_tag((request.get_json(silent=True) or {}).get("tag", "")) if not tag: return jsonify(error="bad tag (a-z, 0-9, _, -, до 32 символов)"), 400 with connect() as con: exists = con.execute("SELECT 1 FROM videos WHERE filename = ?", (filename,)).fetchone() if exists is None: return jsonify(error="not found"), 404 con.execute("INSERT OR IGNORE INTO tags(name) VALUES (?)", (tag,)) tag_id = con.execute("SELECT id FROM tags WHERE name = ?", (tag,)).fetchone()["id"] con.execute( "INSERT OR IGNORE INTO video_tags(filename, tag_id) VALUES (?, ?)", (filename, tag_id), ) tags = [ r["name"] for r in con.execute( "SELECT t.name FROM video_tags vt JOIN tags t ON t.id = vt.tag_id " "WHERE vt.filename = ? ORDER BY t.name", (filename,), ) ] return jsonify(tags=tags), 201 @app.post("/api/scan") def rescan(): from scanner import scan_directory added, total = scan_directory(VIDEO_DIR) return jsonify(added=added, total=total) @app.get("/") def index(): return send_from_directory(FRONTEND_DIR, "index.html") @app.get("/") def frontend_files(path: str): if path.startswith("api/"): return jsonify(error="not found"), 404 full = os.path.join(FRONTEND_DIR, path) if os.path.isfile(full): return send_from_directory(FRONTEND_DIR, path) if os.path.splitext(path)[1].lower() in ALLOWED_EXT: return send_from_directory(VIDEO_DIR, unquote(path)) return send_from_directory(FRONTEND_DIR, "index.html") if __name__ == "__main__": init_db() if os.environ.get("FLASK_DEBUG"): app.run(host="127.0.0.1", port=8000, debug=True) else: from waitress import serve print("Serving on http://127.0.0.1:8000") serve(app, host="127.0.0.1", port=8000)