You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

212 lines
6.2 KiB
Python

# app.py — Streamaster, DB-Only, Clean
import os, time, json, math, hashlib, subprocess
from concurrent.futures import ThreadPoolExecutor
3 months ago
import psycopg2.extras
from flask import Flask, render_template, request, jsonify, send_file
3 months ago
from config import get_local_db_connection
3 months ago
# ───────── CONFIG ───────── #
app = Flask(__name__)
3 months ago
3 months ago
THUMB_DIR = "static/thumbnails"
VIDEOS_PER_PAGE = 40
DASHBOARD_PER_PAGE = 100
THUMB_WIDTH = 640
FF_QUALITY = "80"
3 months ago
os.makedirs(THUMB_DIR, exist_ok=True)
3 months ago
VIDEO_DIRS = [
"U:/encoded",
"U:/count_sorted",
"E:/streamaster/downloaded"
]
def find_video_file(filename: str) -> str | None:
for directory in VIDEO_DIRS:
candidate = os.path.join(directory, filename)
if os.path.exists(candidate):
return candidate
return None
3 months ago
# ───────── DB HELPER ───────── #
def db_get_videos():
conn, cur = get_local_db_connection()
3 months ago
cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor)
3 months ago
cur.execute("""
SELECT
video_id, username, site AS platform,
filepath, size, duration, gender,
3 months ago
created_at, updated_at, thumbnail
3 months ago
FROM videos
3 months ago
WHERE status != 'missing'
3 months ago
""")
rows = cur.fetchall()
cur.close(); conn.close()
3 months ago
return [dict(r) for r in rows]
3 months ago
# ───────── THUMBNAIL HELPERS ───────── #
3 months ago
def _hashed_thumb_path(video_id: str):
h = hashlib.md5(video_id.encode()).hexdigest()
sub1, sub2 = h[:2], h[2:4]
path = os.path.join(THUMB_DIR, sub1, sub2)
os.makedirs(path, exist_ok=True)
return os.path.join(path, f"{video_id}.webp")
def _gen_thumb_cmd(src: str, dest: str):
return [
"ffmpeg", "-y", "-loglevel", "error",
"-ss", "0", "-i", src,
"-vframes", "1",
"-vf", f"thumbnail,scale={THUMB_WIDTH}:-1",
"-q:v", FF_QUALITY,
dest
3 months ago
]
3 months ago
def generate_thumbnails_for_videos(videos):
tasks = []
3 months ago
for v in videos:
3 months ago
video_id = v.get("video_id")
filepath = v.get("filepath")
thumb_path = _hashed_thumb_path(video_id)
3 months ago
if not filepath:
print(f"⚠️ Skipping {video_id}: missing filepath")
continue
if not os.path.exists(filepath):
print(f"⚠️ Skipping {video_id}: file not found → {filepath}")
continue
if not os.path.exists(thumb_path):
3 months ago
tasks.append((filepath, thumb_path))
v["thumbnail"] = thumb_path
3 months ago
if tasks:
with ThreadPoolExecutor(max_workers=os.cpu_count() * 2) as exe:
3 months ago
list(exe.map(lambda t: subprocess.run(
_gen_thumb_cmd(*t),
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
), tasks))
# ───────── CACHE BUILDER ───────── #
def build_cache():
3 months ago
videos = db_get_videos()
grouped = {}
3 months ago
for v in videos:
key = (v["username"], v["platform"])
grouped.setdefault(key, []).append(v)
3 months ago
storage_usage, avg_sizes, video_map = {}, {}, {}
3 months ago
for (username, platform), vids in grouped.items():
3 months ago
key = f"{username}::{platform}"
3 months ago
total_gb = 0
for v in vids:
try:
total_gb += float(v.get("size", 0) or 0) / 1024
3 months ago
except ValueError:
print(f"⚠️ Invalid size for video {v.get('video_id')}: {v.get('size')}")
3 months ago
storage_usage[key] = {
"total_size": total_gb,
"video_count": len(vids)
}
avg_sizes[key] = total_gb / len(vids) if vids else 0
video_map[key] = vids
3 months ago
# generate_thumbnails_for_videos(videos)
3 months ago
return {
"timestamp": time.time(),
"videos": video_map,
"storage_usage": storage_usage,
"avg_sizes": avg_sizes
3 months ago
}
# ───────── ROUTES ───────── #
3 months ago
@app.route("/")
def dashboard():
3 months ago
cache = build_cache()
3 months ago
query = request.args.get("q", "").lower().strip()
3 months ago
sorted_usage = sorted(
cache["storage_usage"].items(),
key=lambda x: x[1]["total_size"],
reverse=True
)
3 months ago
if query:
3 months ago
sorted_usage = [e for e in sorted_usage if query in e[0].lower()]
3 months ago
page = max(1, int(request.args.get("page", 1)))
3 months ago
total_pages = max(1, math.ceil(len(sorted_usage) / DASHBOARD_PER_PAGE))
start = (page - 1) * DASHBOARD_PER_PAGE
paginated = sorted_usage[start:start + DASHBOARD_PER_PAGE]
3 months ago
return render_template(
"analytics.html",
storage_usage=paginated,
avg_sizes=cache["avg_sizes"],
page=page,
total_pages=total_pages,
query=query
)
3 months ago
@app.route("/refresh")
def refresh():
cache = build_cache()
return jsonify({
"status": "ok",
"videos": sum(x["video_count"] for x in cache["storage_usage"].values()),
"updated": time.ctime(cache["timestamp"])
})
3 months ago
@app.route("/user/<username>")
def user_page(username):
3 months ago
cache = build_cache()
3 months ago
videos = [
v | {"platform": key.split("::")[1]}
for key, vids in cache["videos"].items()
if key.split("::")[0] == username
for v in vids
]
3 months ago
page = max(1, int(request.args.get("page", 1)))
3 months ago
total_pages = max(1, math.ceil(len(videos) / VIDEOS_PER_PAGE))
start = (page - 1) * VIDEOS_PER_PAGE
paginated = videos[start:start + VIDEOS_PER_PAGE]
return render_template(
"user_page.html",
username=username,
videos=paginated,
page=page,
total_pages=total_pages
)
3 months ago
3 months ago
@app.route("/video/stream/<video_id>")
def stream_video(video_id):
3 months ago
cache = build_cache()
3 months ago
for vids in cache["videos"].values():
for v in vids:
3 months ago
if v["video_id"] == video_id:
3 months ago
return send_file(v["filepath"], mimetype="video/mp4")
return "Video not found", 404
@app.route("/video/<video_id>")
def view_video(video_id):
3 months ago
cache = build_cache()
3 months ago
for vids in cache["videos"].values():
for v in vids:
3 months ago
if v["video_id"] == video_id:
3 months ago
return render_template("video_view.html", video=v)
return "Video not found", 404
3 months ago
if __name__ == "__main__":
3 months ago
app.run(debug=True)