180 lines
6.1 KiB
Python
180 lines
6.1 KiB
Python
"""Бэкенд 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)
|
||
|