|
|
|
@ -1,20 +1,17 @@
|
|
|
|
# app.py – DB-powered version
|
|
|
|
# app.py – DB-powered version
|
|
|
|
import os, time, json, zlib, math, hashlib, subprocess
|
|
|
|
import os, time, json, zlib, math, hashlib, subprocess
|
|
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
|
|
from datetime import datetime
|
|
|
|
import psycopg2.extras
|
|
|
|
|
|
|
|
|
|
|
|
from flask import (
|
|
|
|
from flask import (
|
|
|
|
Flask, render_template, request, jsonify, send_file
|
|
|
|
Flask, render_template, request, jsonify, send_file
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
from config import connect_redis, get_local_db_connection
|
|
|
|
from config import get_local_db_connection
|
|
|
|
|
|
|
|
|
|
|
|
# ───────── CONFIG ───────── #
|
|
|
|
# ───────── CONFIG ───────── #
|
|
|
|
app = Flask(__name__)
|
|
|
|
app = Flask(__name__)
|
|
|
|
redis = connect_redis()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CACHE_KEY = "video_cache_v3" # bumped because source changed
|
|
|
|
|
|
|
|
META_HASH = "video_meta_v3"
|
|
|
|
|
|
|
|
THUMB_DIR = "static/thumbnails"
|
|
|
|
THUMB_DIR = "static/thumbnails"
|
|
|
|
VIDEOS_PER_PAGE = 40
|
|
|
|
VIDEOS_PER_PAGE = 40
|
|
|
|
DASHBOARD_PER_PAGE = 100
|
|
|
|
DASHBOARD_PER_PAGE = 100
|
|
|
|
@ -24,25 +21,19 @@ FF_QUALITY = "80"
|
|
|
|
os.makedirs(THUMB_DIR, exist_ok=True)
|
|
|
|
os.makedirs(THUMB_DIR, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
|
|
# ───────── DB HELPER ───────── #
|
|
|
|
# ───────── DB HELPER ───────── #
|
|
|
|
|
|
|
|
|
|
|
|
def db_get_videos():
|
|
|
|
def db_get_videos():
|
|
|
|
"""Return list of dicts exactly like the old parser produced."""
|
|
|
|
|
|
|
|
conn, cur = get_local_db_connection()
|
|
|
|
conn, cur = get_local_db_connection()
|
|
|
|
|
|
|
|
cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor)
|
|
|
|
cur.execute("""
|
|
|
|
cur.execute("""
|
|
|
|
SELECT
|
|
|
|
SELECT
|
|
|
|
video_id, username, site AS platform,
|
|
|
|
video_id, username, site AS platform,
|
|
|
|
filepath, size_mb AS size,
|
|
|
|
filepath, size, duration, gender, created_at, updated_at
|
|
|
|
duration,
|
|
|
|
|
|
|
|
gender,
|
|
|
|
|
|
|
|
created_at,
|
|
|
|
|
|
|
|
updated_at
|
|
|
|
|
|
|
|
FROM videos
|
|
|
|
FROM videos
|
|
|
|
""")
|
|
|
|
""")
|
|
|
|
rows = cur.fetchall()
|
|
|
|
rows = cur.fetchall()
|
|
|
|
# Convert psycopg rows → list[dict]
|
|
|
|
|
|
|
|
cols = [desc[0] for desc in cur.description]
|
|
|
|
|
|
|
|
videos = [dict(zip(cols, row)) for row in rows]
|
|
|
|
|
|
|
|
cur.close(); conn.close()
|
|
|
|
cur.close(); conn.close()
|
|
|
|
return videos
|
|
|
|
return [dict(r) for r in rows]
|
|
|
|
|
|
|
|
|
|
|
|
# ───────── THUMB UTILS ───────── #
|
|
|
|
# ───────── THUMB UTILS ───────── #
|
|
|
|
def _hashed_thumb_path(video_id: str):
|
|
|
|
def _hashed_thumb_path(video_id: str):
|
|
|
|
@ -65,11 +56,8 @@ def _gen_thumb_cmd(src: str, dest: str):
|
|
|
|
def generate_thumbnail(task):
|
|
|
|
def generate_thumbnail(task):
|
|
|
|
src, dest = task
|
|
|
|
src, dest = task
|
|
|
|
if not os.path.exists(dest):
|
|
|
|
if not os.path.exists(dest):
|
|
|
|
subprocess.run(_gen_thumb_cmd(src, dest),
|
|
|
|
subprocess.run(_gen_thumb_cmd(src, dest), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
|
|
stdout=subprocess.DEVNULL,
|
|
|
|
|
|
|
|
stderr=subprocess.DEVNULL)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ───────── CACHE BUILDER ───────── #
|
|
|
|
|
|
|
|
def build_cache():
|
|
|
|
def build_cache():
|
|
|
|
videos = db_get_videos()
|
|
|
|
videos = db_get_videos()
|
|
|
|
|
|
|
|
|
|
|
|
@ -84,7 +72,14 @@ def build_cache():
|
|
|
|
|
|
|
|
|
|
|
|
for (username, platform), vids in grouped.items():
|
|
|
|
for (username, platform), vids in grouped.items():
|
|
|
|
key = f"{username}::{platform}"
|
|
|
|
key = f"{username}::{platform}"
|
|
|
|
total_gb = sum(v["size"] for v in vids) / 1024
|
|
|
|
total_gb = 0
|
|
|
|
|
|
|
|
for v in vids:
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
size_mb = float(v.get("size", 0) or 0)
|
|
|
|
|
|
|
|
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] = {
|
|
|
|
storage_usage[key] = {
|
|
|
|
"total_size": total_gb,
|
|
|
|
"total_size": total_gb,
|
|
|
|
"video_count": len(vids)
|
|
|
|
"video_count": len(vids)
|
|
|
|
@ -95,13 +90,9 @@ def build_cache():
|
|
|
|
video_id = v["video_id"]
|
|
|
|
video_id = v["video_id"]
|
|
|
|
thumb_path = _hashed_thumb_path(video_id)
|
|
|
|
thumb_path = _hashed_thumb_path(video_id)
|
|
|
|
|
|
|
|
|
|
|
|
# Meta-cache – use DB updated_at as mtime surrogate
|
|
|
|
if not os.path.exists(thumb_path):
|
|
|
|
meta = redis.hget(META_HASH, video_id)
|
|
|
|
|
|
|
|
if not meta or json.loads(meta)["updated_at"] != str(v["updated_at"]):
|
|
|
|
|
|
|
|
thumb_tasks.append((v["filepath"], thumb_path))
|
|
|
|
thumb_tasks.append((v["filepath"], thumb_path))
|
|
|
|
redis.hset(META_HASH, video_id,
|
|
|
|
|
|
|
|
json.dumps({"updated_at": str(v["updated_at"]),
|
|
|
|
|
|
|
|
"thumb" : thumb_path}))
|
|
|
|
|
|
|
|
v["thumbnail"] = thumb_path
|
|
|
|
v["thumbnail"] = thumb_path
|
|
|
|
|
|
|
|
|
|
|
|
video_map[key] = vids
|
|
|
|
video_map[key] = vids
|
|
|
|
@ -110,33 +101,17 @@ def build_cache():
|
|
|
|
with ThreadPoolExecutor(max_workers=os.cpu_count()*2) as exe:
|
|
|
|
with ThreadPoolExecutor(max_workers=os.cpu_count()*2) as exe:
|
|
|
|
list(exe.map(generate_thumbnail, thumb_tasks))
|
|
|
|
list(exe.map(generate_thumbnail, thumb_tasks))
|
|
|
|
|
|
|
|
|
|
|
|
cache = {
|
|
|
|
return {
|
|
|
|
"timestamp" : time.time(),
|
|
|
|
"timestamp" : time.time(),
|
|
|
|
"videos" : video_map,
|
|
|
|
"videos" : video_map,
|
|
|
|
"storage_usage" : storage_usage,
|
|
|
|
"storage_usage" : storage_usage,
|
|
|
|
"avg_sizes" : avg_sizes
|
|
|
|
"avg_sizes" : avg_sizes
|
|
|
|
}
|
|
|
|
}
|
|
|
|
blob = zlib.compress(json.dumps(cache).encode())
|
|
|
|
|
|
|
|
redis.set(CACHE_KEY, blob)
|
|
|
|
|
|
|
|
with open("video_cache.json.gz", "wb") as f:
|
|
|
|
|
|
|
|
f.write(blob)
|
|
|
|
|
|
|
|
return cache
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_cached_data():
|
|
|
|
|
|
|
|
blob = redis.get(CACHE_KEY)
|
|
|
|
|
|
|
|
if blob:
|
|
|
|
|
|
|
|
return json.loads(zlib.decompress(blob).decode())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if os.path.exists("video_cache.json.gz"):
|
|
|
|
|
|
|
|
with open("video_cache.json.gz", "rb") as f:
|
|
|
|
|
|
|
|
return json.loads(zlib.decompress(f.read()).decode())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return build_cache()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ───────── ROUTES (unchanged logic) ───────── #
|
|
|
|
# ───────── ROUTES (unchanged logic) ───────── #
|
|
|
|
@app.route("/")
|
|
|
|
@app.route("/")
|
|
|
|
def dashboard():
|
|
|
|
def dashboard():
|
|
|
|
cache = get_cached_data()
|
|
|
|
cache = build_cache()
|
|
|
|
query = request.args.get("q", "").lower().strip()
|
|
|
|
query = request.args.get("q", "").lower().strip()
|
|
|
|
|
|
|
|
|
|
|
|
sorted_usage = sorted(
|
|
|
|
sorted_usage = sorted(
|
|
|
|
@ -172,7 +147,7 @@ def refresh():
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/user/<username>")
|
|
|
|
@app.route("/user/<username>")
|
|
|
|
def user_page(username):
|
|
|
|
def user_page(username):
|
|
|
|
cache = get_cached_data()
|
|
|
|
cache = build_cache()
|
|
|
|
|
|
|
|
|
|
|
|
videos = [
|
|
|
|
videos = [
|
|
|
|
v | {"platform": key.split("::")[1]}
|
|
|
|
v | {"platform": key.split("::")[1]}
|
|
|
|
@ -196,7 +171,7 @@ def user_page(username):
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/video/stream/<video_id>")
|
|
|
|
@app.route("/video/stream/<video_id>")
|
|
|
|
def stream_video(video_id):
|
|
|
|
def stream_video(video_id):
|
|
|
|
cache = get_cached_data()
|
|
|
|
cache = build_cache()
|
|
|
|
for vids in cache["videos"].values():
|
|
|
|
for vids in cache["videos"].values():
|
|
|
|
for v in vids:
|
|
|
|
for v in vids:
|
|
|
|
if v["video_id"] == video_id:
|
|
|
|
if v["video_id"] == video_id:
|
|
|
|
@ -205,7 +180,7 @@ def stream_video(video_id):
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/video/<video_id>")
|
|
|
|
@app.route("/video/<video_id>")
|
|
|
|
def view_video(video_id):
|
|
|
|
def view_video(video_id):
|
|
|
|
cache = get_cached_data()
|
|
|
|
cache = build_cache()
|
|
|
|
for vids in cache["videos"].values():
|
|
|
|
for vids in cache["videos"].values():
|
|
|
|
for v in vids:
|
|
|
|
for v in vids:
|
|
|
|
if v["video_id"] == video_id:
|
|
|
|
if v["video_id"] == video_id:
|
|
|
|
@ -213,4 +188,4 @@ def view_video(video_id):
|
|
|
|
return "Video not found", 404
|
|
|
|
return "Video not found", 404
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
if __name__ == "__main__":
|
|
|
|
app.run(debug=True)
|
|
|
|
app.run(debug=True)
|