Files
vv_ext/app.py
T
2026-10-09 22:15:22 +03:00

180 lines
6.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Бэкенд 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/<path:raw_file>/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/<path:raw_file>/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("/<path:path>")
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)