first commit
This commit is contained in:
commit
865786f210
16 files changed
+743
No files matched your search
@@ -0,0 +1,179 @@
|
||||
"""Бэкенд 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)
|
||||
|
||||
Reference in new issue
Block a user