first commit

This commit is contained in:
micialware committed 2026-10-09 22:15:22 +03:00
commit 865786f210
16 files changed
+743

No files matched your search

+179
View File
@@ -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)