|
|
|
|
@ -1,12 +1,8 @@
|
|
|
|
|
# app.py – DB-powered version
|
|
|
|
|
import os, time, json, zlib, math, hashlib, subprocess
|
|
|
|
|
# app.py — Streamaster, DB-Only, Clean
|
|
|
|
|
import os, time, json, math, hashlib, subprocess
|
|
|
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
|
|
|
import psycopg2.extras
|
|
|
|
|
|
|
|
|
|
from flask import (
|
|
|
|
|
Flask, render_template, request, jsonify, send_file
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
from flask import Flask, render_template, request, jsonify, send_file
|
|
|
|
|
from config import get_local_db_connection
|
|
|
|
|
|
|
|
|
|
# ───────── CONFIG ───────── #
|
|
|
|
|
@ -21,21 +17,21 @@ FF_QUALITY = "80"
|
|
|
|
|
os.makedirs(THUMB_DIR, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
# ───────── DB HELPER ───────── #
|
|
|
|
|
|
|
|
|
|
def db_get_videos():
|
|
|
|
|
conn, cur = get_local_db_connection()
|
|
|
|
|
cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor)
|
|
|
|
|
cur.execute("""
|
|
|
|
|
SELECT
|
|
|
|
|
video_id, username, site AS platform,
|
|
|
|
|
filepath, size, duration, gender, created_at, updated_at
|
|
|
|
|
filepath, size, duration, gender,
|
|
|
|
|
created_at, updated_at
|
|
|
|
|
FROM videos
|
|
|
|
|
""")
|
|
|
|
|
rows = cur.fetchall()
|
|
|
|
|
cur.close(); conn.close()
|
|
|
|
|
return [dict(r) for r in rows]
|
|
|
|
|
|
|
|
|
|
# ───────── THUMB UTILS ───────── #
|
|
|
|
|
# ───────── THUMBNAIL HELPERS ───────── #
|
|
|
|
|
def _hashed_thumb_path(video_id: str):
|
|
|
|
|
h = hashlib.md5(video_id.encode()).hexdigest()
|
|
|
|
|
sub1, sub2 = h[:2], h[2:4]
|
|
|
|
|
@ -53,62 +49,60 @@ def _gen_thumb_cmd(src: str, dest: str):
|
|
|
|
|
dest
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
def generate_thumbnail(task):
|
|
|
|
|
src, dest = task
|
|
|
|
|
if not os.path.exists(dest):
|
|
|
|
|
subprocess.run(_gen_thumb_cmd(src, dest), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
|
|
|
def generate_thumbnails_for_videos(videos):
|
|
|
|
|
tasks = []
|
|
|
|
|
for v in videos:
|
|
|
|
|
video_id = v["video_id"]
|
|
|
|
|
thumb_path = _hashed_thumb_path(video_id)
|
|
|
|
|
|
|
|
|
|
if not os.path.exists(thumb_path):
|
|
|
|
|
tasks.append((v["filepath"], thumb_path))
|
|
|
|
|
|
|
|
|
|
v["thumbnail"] = thumb_path
|
|
|
|
|
|
|
|
|
|
if tasks:
|
|
|
|
|
with ThreadPoolExecutor(max_workers=os.cpu_count() * 2) as exe:
|
|
|
|
|
list(exe.map(lambda t: subprocess.run(_gen_thumb_cmd(*t), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL), tasks))
|
|
|
|
|
|
|
|
|
|
# ───────── CACHE BUILDER ───────── #
|
|
|
|
|
def build_cache():
|
|
|
|
|
videos = db_get_videos()
|
|
|
|
|
|
|
|
|
|
# group by (username, platform)
|
|
|
|
|
grouped = {}
|
|
|
|
|
|
|
|
|
|
for v in videos:
|
|
|
|
|
key = (v["username"], v["platform"])
|
|
|
|
|
grouped.setdefault(key, []).append(v)
|
|
|
|
|
|
|
|
|
|
storage_usage, avg_sizes, video_map = {}, {}, {}
|
|
|
|
|
thumb_tasks = []
|
|
|
|
|
|
|
|
|
|
for (username, platform), vids in grouped.items():
|
|
|
|
|
key = f"{username}::{platform}"
|
|
|
|
|
|
|
|
|
|
total_gb = 0
|
|
|
|
|
for v in vids:
|
|
|
|
|
try:
|
|
|
|
|
size_mb = float(v.get("size", 0) or 0)
|
|
|
|
|
total_gb += float(v.get("size", 0) or 0) / 1024
|
|
|
|
|
except ValueError:
|
|
|
|
|
print(f"⚠️ Invalid size for video {v.get('video_id')}: {v.get('size')}")
|
|
|
|
|
size_mb = 0
|
|
|
|
|
total_gb += size_mb / 1024
|
|
|
|
|
|
|
|
|
|
storage_usage[key] = {
|
|
|
|
|
"total_size": total_gb,
|
|
|
|
|
"video_count": len(vids)
|
|
|
|
|
}
|
|
|
|
|
avg_sizes[key] = total_gb / len(vids) if vids else 0
|
|
|
|
|
|
|
|
|
|
for v in vids:
|
|
|
|
|
video_id = v["video_id"]
|
|
|
|
|
thumb_path = _hashed_thumb_path(video_id)
|
|
|
|
|
|
|
|
|
|
if not os.path.exists(thumb_path):
|
|
|
|
|
thumb_tasks.append((v["filepath"], thumb_path))
|
|
|
|
|
|
|
|
|
|
v["thumbnail"] = thumb_path
|
|
|
|
|
|
|
|
|
|
video_map[key] = vids
|
|
|
|
|
|
|
|
|
|
if thumb_tasks:
|
|
|
|
|
with ThreadPoolExecutor(max_workers=os.cpu_count()*2) as exe:
|
|
|
|
|
list(exe.map(generate_thumbnail, thumb_tasks))
|
|
|
|
|
generate_thumbnails_for_videos(videos)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"timestamp" : time.time(),
|
|
|
|
|
"videos" : video_map,
|
|
|
|
|
"storage_usage" : storage_usage,
|
|
|
|
|
"avg_sizes" : avg_sizes
|
|
|
|
|
"timestamp": time.time(),
|
|
|
|
|
"videos": video_map,
|
|
|
|
|
"storage_usage": storage_usage,
|
|
|
|
|
"avg_sizes": avg_sizes
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# ───────── ROUTES (unchanged logic) ───────── #
|
|
|
|
|
# ───────── ROUTES ───────── #
|
|
|
|
|
@app.route("/")
|
|
|
|
|
def dashboard():
|
|
|
|
|
cache = build_cache()
|
|
|
|
|
@ -129,26 +123,25 @@ def dashboard():
|
|
|
|
|
|
|
|
|
|
return render_template(
|
|
|
|
|
"analytics.html",
|
|
|
|
|
storage_usage = paginated,
|
|
|
|
|
avg_sizes = cache["avg_sizes"],
|
|
|
|
|
page = page,
|
|
|
|
|
total_pages = total_pages,
|
|
|
|
|
query = query
|
|
|
|
|
storage_usage=paginated,
|
|
|
|
|
avg_sizes=cache["avg_sizes"],
|
|
|
|
|
page=page,
|
|
|
|
|
total_pages=total_pages,
|
|
|
|
|
query=query
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@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"])
|
|
|
|
|
"status": "ok",
|
|
|
|
|
"videos": sum(x["video_count"] for x in cache["storage_usage"].values()),
|
|
|
|
|
"updated": time.ctime(cache["timestamp"])
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
@app.route("/user/<username>")
|
|
|
|
|
def user_page(username):
|
|
|
|
|
cache = build_cache()
|
|
|
|
|
|
|
|
|
|
videos = [
|
|
|
|
|
v | {"platform": key.split("::")[1]}
|
|
|
|
|
for key, vids in cache["videos"].items()
|
|
|
|
|
@ -163,10 +156,10 @@ def user_page(username):
|
|
|
|
|
|
|
|
|
|
return render_template(
|
|
|
|
|
"user_page.html",
|
|
|
|
|
username = username,
|
|
|
|
|
videos = paginated,
|
|
|
|
|
page = page,
|
|
|
|
|
total_pages = total_pages
|
|
|
|
|
username=username,
|
|
|
|
|
videos=paginated,
|
|
|
|
|
page=page,
|
|
|
|
|
total_pages=total_pages
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@app.route("/video/stream/<video_id>")
|
|
|
|
|
|