diff --git a/app.py b/app.py index 19da3e5..9f89da6 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,6 @@ # 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 config import get_local_db_connection +from flask import Flask +import os # ───────── CONFIG ───────── # app = Flask(__name__) @@ -29,184 +26,14 @@ def find_video_file(filename: str) -> str | None: return candidate return None -# ───────── 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, thumbnail - FROM videos - WHERE status != 'missing' - """) - rows = cur.fetchall() - cur.close(); conn.close() - return [dict(r) for r in rows] +# ───────── BLUEPRINTS ───────── # +from routes.web import web +from routes.api import api +from helpers.favorites import db_init_favorites_table -# ───────── THUMBNAIL HELPERS ───────── # -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 - ] - -def generate_thumbnails_for_videos(videos): - tasks = [] - - for v in videos: - video_id = v.get("video_id") - filepath = v.get("filepath") - thumb_path = _hashed_thumb_path(video_id) - - 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): - tasks.append((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() - grouped = {} - - for v in videos: - key = (v["username"], v["platform"]) - grouped.setdefault(key, []).append(v) - - storage_usage, avg_sizes, video_map = {}, {}, {} - - for (username, platform), vids in grouped.items(): - key = f"{username}::{platform}" - - total_gb = 0 - for v in vids: - try: - 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')}") - - 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 - - # generate_thumbnails_for_videos(videos) - - return { - "timestamp": time.time(), - "videos": video_map, - "storage_usage": storage_usage, - "avg_sizes": avg_sizes - } - -# ───────── ROUTES ───────── # -@app.route("/") -def dashboard(): - cache = build_cache() - query = request.args.get("q", "").lower().strip() - - sorted_usage = sorted( - cache["storage_usage"].items(), - key=lambda x: x[1]["total_size"], - reverse=True - ) - if query: - sorted_usage = [e for e in sorted_usage if query in e[0].lower()] - - page = max(1, int(request.args.get("page", 1))) - 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] - - return render_template( - "analytics.html", - 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"]) - }) - -@app.route("/user/") -def user_page(username): - cache = build_cache() - videos = [ - v | {"platform": key.split("::")[1]} - for key, vids in cache["videos"].items() - if key.split("::")[0] == username - for v in vids - ] - - page = max(1, int(request.args.get("page", 1))) - 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 - ) - -@app.route("/video/stream/") -def stream_video(video_id): - cache = build_cache() - for vids in cache["videos"].values(): - for v in vids: - if v["video_id"] == video_id: - return send_file(v["filepath"], mimetype="video/mp4") - return "Video not found", 404 - -@app.route("/video/") -def view_video(video_id): - cache = build_cache() - for vids in cache["videos"].values(): - for v in vids: - if v["video_id"] == video_id: - return render_template("video_view.html", video=v) - return "Video not found", 404 +app.register_blueprint(web) +app.register_blueprint(api) if __name__ == "__main__": + db_init_favorites_table() app.run(debug=True) \ No newline at end of file diff --git a/cleanup.py b/cleanup.py index ef85748..b565230 100644 --- a/cleanup.py +++ b/cleanup.py @@ -198,12 +198,13 @@ if __name__ == '__main__': print("🔍 Scanning for missing data...") fill_missing_filepaths(cursor, conn) - # mark_missing_videos(cursor, conn) + # fill_missing_durations(cursor, conn) + # fill_missing_sizes(cursor, conn) + # generate_thumbnails_for_videos(cursor, conn) + # fill_missing_hashes(cursor, conn) - fill_missing_sizes(cursor, conn) - fill_missing_durations(cursor, conn) + # mark_missing_videos(cursor, conn) # fill_missing_gender(cursor, conn) - generate_thumbnails_for_videos(cursor, conn) cursor.close() diff --git a/helpers/__init__.py b/helpers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/helpers/cache.py b/helpers/cache.py new file mode 100644 index 0000000..b5f5828 --- /dev/null +++ b/helpers/cache.py @@ -0,0 +1,38 @@ + +# ───────── CACHE BUILDER ───────── # +def build_cache(): + videos = db_get_videos() + grouped = {} + + for v in videos: + key = (v["username"], v["platform"]) + grouped.setdefault(key, []).append(v) + + storage_usage, avg_sizes, video_map = {}, {}, {} + + for (username, platform), vids in grouped.items(): + key = f"{username}::{platform}" + + total_gb = 0 + for v in vids: + try: + 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')}") + + 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 + + # generate_thumbnails_for_videos(videos) + + return { + "timestamp": time.time(), + "videos": video_map, + "storage_usage": storage_usage, + "avg_sizes": avg_sizes + } \ No newline at end of file diff --git a/helpers/db.py b/helpers/db.py new file mode 100644 index 0000000..5ac4ad7 --- /dev/null +++ b/helpers/db.py @@ -0,0 +1,56 @@ +# ───────── DB HELPER ───────── # +def db_get_videos(username = None): + conn, cur = get_local_db_connection() + cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) + + query = "SELECT video_id, username, site AS platform, filepath, size, duration, gender, created_at, updated_at, thumbnail FROM videos WHERE status != 'missing'" + if username: + query += f" AND username = '{username}'" + + if True: + query += " ORDER BY created_at DESC" + + cur.execute(query) + + rows = cur.fetchall() + cur.close(); conn.close() + return [dict(r) for r in rows] + +def db_get_video(video_id: str): + 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, thumbnail + FROM videos + WHERE video_id = %s + """, (video_id,)) + row = cur.fetchone() + cur.close(); conn.close() + return dict(row) + +def db_get_recent(page: int, per_page: int): + offset = (page - 1) * per_page + conn, cur = get_local_db_connection() + cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) + + # Order by created_at desc (fallback to updated_at if you prefer) + cur.execute(""" + SELECT + video_id, username, site AS platform, + filepath, size, duration, gender, + created_at, updated_at, thumbnail + FROM videos + WHERE status != 'missing' + ORDER BY created_at DESC NULLS LAST, updated_at DESC NULLS LAST + LIMIT %s OFFSET %s + """, (per_page, offset)) + rows = [dict(r) for r in cur.fetchall()] + + cur.execute("SELECT COUNT(*) FROM videos WHERE status != 'missing'") + total = cur.fetchone()[0] + + cur.close(); conn.close() + return rows, total diff --git a/helpers/favorites.py b/helpers/favorites.py new file mode 100644 index 0000000..45faca1 --- /dev/null +++ b/helpers/favorites.py @@ -0,0 +1,75 @@ + + + + +# ───────── FAVORITES ───────── # +def db_init_favorites_table(): + conn, cur = get_local_db_connection() + cur = conn.cursor() + cur.execute(""" + CREATE TABLE IF NOT EXISTS favorites ( + video_id TEXT PRIMARY KEY, + created_at TIMESTAMPTZ DEFAULT NOW() + ) + """) + conn.commit() + cur.close(); conn.close() + +def db_get_fav_set(): + conn, cur = get_local_db_connection() + cur = conn.cursor() + cur.execute("SELECT video_id FROM favorites") + favs = {row[0] for row in cur.fetchall()} + cur.close(); conn.close() + return favs + +def db_toggle_fav(video_id: str): + conn, cur = get_local_db_connection() + cur = conn.cursor() + # Try to delete; if nothing deleted, insert + cur.execute("DELETE FROM favorites WHERE video_id = %s", (video_id,)) + if cur.rowcount == 0: + cur.execute("INSERT INTO favorites (video_id) VALUES (%s)", (video_id,)) + conn.commit() + cur.close(); conn.close() + return True # now favorited + conn.commit() + cur.close(); conn.close() + return False # now unfavorited + +def mark_favorites(videos: list[dict]): + """Adds is_favorite: bool to each video dict in-place.""" + favs = db_get_fav_set() + for v in videos: + vid = str(v.get("video_id")) + v["is_favorite"] = (vid in favs) + return videos + +def db_get_favorites(page: int, per_page: int): + offset = (page - 1) * per_page + conn, cur = get_local_db_connection() + cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) + + cur.execute(""" + SELECT + v.video_id, v.username, v.site AS platform, + v.filepath, v.size, v.duration, v.gender, + v.created_at, v.updated_at, v.thumbnail + FROM favorites f + JOIN videos v ON v.video_id = f.video_id + WHERE v.status != 'missing' + ORDER BY f.created_at DESC NULLS LAST, v.created_at DESC NULLS LAST + LIMIT %s OFFSET %s + """, (per_page, offset)) + rows = [dict(r) for r in cur.fetchall()] + + cur.execute(""" + SELECT COUNT(*) + FROM favorites f + JOIN videos v ON v.video_id = f.video_id + WHERE v.status != 'missing' + """) + total = cur.fetchone()[0] + + cur.close(); conn.close() + return rows, total diff --git a/helpers/thumbnails.py b/helpers/thumbnails.py new file mode 100644 index 0000000..202528b --- /dev/null +++ b/helpers/thumbnails.py @@ -0,0 +1,46 @@ +# ───────── THUMBNAIL HELPERS ───────── # +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 + ] + +def generate_thumbnails_for_videos(videos): + tasks = [] + + for v in videos: + video_id = v.get("video_id") + filepath = v.get("filepath") + thumb_path = _hashed_thumb_path(video_id) + + 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): + tasks.append((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)) diff --git a/organize_data.py b/organize_data.py new file mode 100644 index 0000000..834710a --- /dev/null +++ b/organize_data.py @@ -0,0 +1,138 @@ +from archiveConfig import get_local_db_connection +from psycopg2.extras import execute_values +from datetime import datetime +import uuid, shutil, json, os +from tqdm import tqdm + +DATA_DIR = 'data' +DOWNLOAD_DIR = 'downloaded' + +conn, cursor = get_local_db_connection() + +def is_valid_uuid(val: str, version=None) -> bool: + try: + u = uuid.UUID(val, version=version) if version else uuid.UUID(val) + return str(u) == val.lower() # Match exact input (handles casing) + except (ValueError, AttributeError, TypeError): + return False + +def parse_json_file(filepath): + with open(filepath, 'r', encoding='utf-8') as f: + data = json.load(f) + + if "createdAt" in data: + date = data.get("createdAt") + elif "date" in data: + date = data.get("date") + + if date: + created_at = datetime.strptime(date, "%Y-%m-%d %H:%M:%S") + else: + created_at = None + print(f"⚠️ No createdAt or date found in {filepath}") + + if "updatedAt" in data: + updated_at = datetime.strptime(data.get("updatedAt"), "%Y-%m-%d %H:%M:%S") + else: + updated_at = created_at + + video_id = os.path.splitext(os.path.basename(filepath))[0] + if not is_valid_uuid(video_id): + print(f"⚠️ Invalid video_id: {video_id}") + return + + parsed_data = { + 'video_id': video_id, + 'username': data.get("username"), + 'site': data.get("site"), + 'gender': data.get("gender"), + 'size': data.get("size") if data.get("size") else 0, + 'duration': data.get("duration") if data.get("duration") else 0, + 'filepath': data.get("filepath"), + 'jsonpath': data.get("jsonpath"), + 'hash': None, # You can add hash calculation here if needed + 'created_at': created_at, + 'updated_at': updated_at + } + + return parsed_data + +def insert_data(all_data): + query = """ + INSERT INTO videos ( + video_id, username, site, gender, size, duration, + filepath, hash, created_at, updated_at + ) + VALUES %s + ON CONFLICT (video_id) DO NOTHING; + """ + values = [ + ( + d['video_id'], d['username'], d['site'], d['gender'], + d['size'], d['duration'], d['filepath'], + d['hash'], d['created_at'], d['updated_at'] + ) + for d in all_data + ] + execute_values(cursor, query, values) + conn.commit() + print(f"✅ Inserted {cursor.rowcount} new records.") + +def get_files(dir): + files = [] + for root, _, filenames in os.walk(dir): + for filename in filenames: + if filename.endswith('.json'): + files.append(os.path.join(root, filename)) + return files + +def main(): + all_records = [] + + data_files = [f for f in get_files(DOWNLOAD_DIR) if f.endswith('.json')] + + with tqdm(data_files, desc="Processing files", unit="file") as t: + for filepath in data_files: + t.update(1) + try: + record = parse_json_file(filepath) + all_records.append(record) + except Exception as e: + print(f"❌ Failed to process {filepath}: {e}") + + if all_records: + insert_data(all_records) + else: + print("⚠️ No new records to insert.") + +def check_and_move(): + db_ids = get_video_ids_from_db() + moved = 0 + + for path in get_json_files(DOWNLOAD_DIR): + video_id = os.path.splitext(os.path.basename(path))[0] + if video_id in db_ids: + output_path = os.path.join(DATA_DIR, os.path.basename(path)) + if os.path.exists(output_path): + print(f"⚠️ Skipping {path} because it already exists in {DOWNLOAD_DIR}/") + continue + shutil.move(path, output_path) + moved += 1 + + print(f"✅ Moved {moved} files to {DOWNLOAD_DIR}/") + +# Get all existing video IDs +def get_video_ids_from_db(): + cursor.execute("SELECT video_id FROM videos;") + return {row['video_id'] for row in cursor.fetchall()} + +# Iterate files +def get_json_files(dir): + for root, _, files in os.walk(dir): + for file in files: + if file.endswith('.json'): + yield os.path.join(root, file) + +if __name__ == '__main__': + main() + check_and_move() \ No newline at end of file diff --git a/routes/__init__.py b/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/routes/api.py b/routes/api.py new file mode 100644 index 0000000..fa7f47d --- /dev/null +++ b/routes/api.py @@ -0,0 +1,53 @@ +@app.route('/open-folder', methods=['POST']) +def open_folder(): + data = request.json + file_path = data.get("file_path") + file_path = os.path.abspath(file_path) + dir_path = os.path.dirname(file_path) + + if not file_path or not os.path.exists(file_path): + return jsonify({"error": "File not found"}), 404 + + # Open folder in Explorer and select file + subprocess.run(f'explorer {dir_path}') + return jsonify({"success": True}) + + +@app.route("/api/fav/toggle/", methods=["POST"]) +def api_fav_toggle(video_id): + # Optional: validate video exists + try: + _ = db_get_video(video_id) + except Exception: + return jsonify({"error": "video not found"}), 404 + + is_fav = db_toggle_fav(video_id) + return jsonify({"ok": True, "video_id": video_id, "is_favorite": is_fav}) + +@app.route("/api/fav/list") +def api_fav_list(): + return jsonify({"favorites": sorted(list(db_get_fav_set()))}) + +@app.route('/delete-file', methods=['POST']) +def delete_file(): + data = request.json + file_path = data.get("file_path") + video_id = data.get("video_id") + + if not file_path or not os.path.exists(file_path): + return jsonify({"error": "File not found"}), 404 + + try: + # delete from filesystem + os.remove(file_path) + + # optional: mark video as missing in DB + conn, cur = get_local_db_connection() + cur = conn.cursor() + cur.execute("UPDATE videos SET status = 'missing' WHERE video_id = %s", (video_id,)) + conn.commit() + cur.close(); conn.close() + + return jsonify({"success": True}) + except Exception as e: + return jsonify({"error": str(e)}), 500 diff --git a/routes/web.py b/routes/web.py new file mode 100644 index 0000000..f0ce9b2 --- /dev/null +++ b/routes/web.py @@ -0,0 +1,134 @@ +from flask import Blueprint, render_template, request, send_file +from helpers.db import db_get_videos, db_get_video, db_get_recent +from helpers.favorites import mark_favorites, db_get_favorites, db_get_fav_set +from helpers.cache import build_cache + +web = Blueprint("web", __name__) + +@web.route("/") +def dashboard(): + cache = build_cache() + query = request.args.get("q", "").lower().strip() + sort = request.args.get("sort", "total_size") # user | platform | total_size | video_count | avg_size + dir_ = request.args.get("dir", "desc") # asc | desc + reverse = (dir_ == "desc") + + # Start with list of (key, stats) + items = list(cache["storage_usage"].items()) + + # Search (by user) + if query: + items = [e for e in items if query in e[0].split("::")[0].lower()] + + # Sorting + def k_user(x): return x[0].split("::")[0].lower() + def k_platform(x): return x[0].split("::")[1].lower() + def k_total(x): return x[1]["total_size"] + def k_count(x): return x[1]["video_count"] + def k_avg(x): return cache["avg_sizes"][x[0]] + + key_map = { + "user": k_user, + "platform": k_platform, + "total_size": k_total, + "video_count": k_count, + "avg_size": k_avg, + } + items.sort(key=key_map.get(sort, k_total), reverse=reverse) + + # Pagination + page = max(1, int(request.args.get("page", 1))) + total_pages = max(1, math.ceil(len(items) / DASHBOARD_PER_PAGE)) + start = (page - 1) * DASHBOARD_PER_PAGE + paginated = items[start:start + DASHBOARD_PER_PAGE] + + return render_template( + "main.html", + storage_usage=paginated, + avg_sizes=cache["avg_sizes"], + page=page, + total_pages=total_pages, + query=query, + sort=sort, + dir=dir_ + ) + + + +@web.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"]) + }) + +@web.route("/user/") +def user_page(username): + videos = db_get_videos(username=username) + + page = max(1, int(request.args.get("page", 1))) + total_pages = max(1, math.ceil(len(videos) / VIDEOS_PER_PAGE)) + start = (page - 1) * VIDEOS_PER_PAGE + paginated = videos[start:start + VIDEOS_PER_PAGE] + mark_favorites(paginated) + + return render_template( + "user_page.html", + username=username, + videos=paginated, + page=page, + total_pages=total_pages + ) + +@web.route("/video/stream/") +def stream_video(video_id): + video = db_get_video(video_id) + if video: + return send_file(video["filepath"], mimetype="video/mp4") + return "Video not found", 404 + +@web.route("/video/") +def view_video(video_id): + video = db_get_video(video_id) + if video: + video["is_favorite"] = (video["video_id"] in db_get_fav_set()) + return render_template("video_view.html", video=video) + return "Video not found", 404 + +@web.route("/recent") +def recent(): + page = max(1, int(request.args.get("page", 1))) + per_page = VIDEOS_PER_PAGE + + videos, total = db_get_recent(page, per_page) + total_pages = max(1, math.ceil(total / per_page)) + + mark_favorites(videos) + + return render_template( + "recent.html", + videos=videos, + page=page, + total_pages=total_pages + ) + +@web.route("/favorites") +def favorites_page(): + page = max(1, int(request.args.get("page", 1))) + per_page = VIDEOS_PER_PAGE + + videos, total = db_get_favorites(page, per_page) + total_pages = max(1, math.ceil(total / per_page)) + + # tag stars (not strictly necessary since everything here is fav, but keeps UI consistent) + mark_favorites(videos) + + return render_template( + "favorites.html", + videos=videos, + page=page, + total_pages=total_pages + ) + diff --git a/templates/analytics.html b/templates/analytics.html deleted file mode 100644 index 99e821a..0000000 --- a/templates/analytics.html +++ /dev/null @@ -1,168 +0,0 @@ - - - - - - 📊 Video Storage Analytics - - - - -

📊 Video Storage Analytics

- - - - - - - - - - - - - - - {% for key, stats in storage_usage %} - {% set user, platform = key.split("::") %} - - - - - - - - {% endfor %} - -
UserPlatformTotal Storage (GB)Video CountAvg Size per Video (GB)
{{ user }}{{ platform }}{{ "%.2f"|format(stats.total_size) }}{{ stats.video_count }}{{ "%.2f"|format(avg_sizes[key]) }}
- - - - - - - \ No newline at end of file diff --git a/templates/favorites.html b/templates/favorites.html new file mode 100644 index 0000000..cd52714 --- /dev/null +++ b/templates/favorites.html @@ -0,0 +1,173 @@ + + + + + + ★ Favorites + + + + +

★ Favorites

+ ⬅ Back to Dashboard + +
+ {% for video in videos %} +
+
+ + Thumbnail + + + +
+

{{ "%.2f"|format(video.size/1024) }} GB

+
+ {% endfor %} +
+ + + + + + + \ No newline at end of file diff --git a/templates/main.html b/templates/main.html new file mode 100644 index 0000000..49e4aed --- /dev/null +++ b/templates/main.html @@ -0,0 +1,162 @@ + + + + + 📊 Video Storage Analytics + + + + + +

📊 Video Storage Analytics

+ +
+ + + +
+ + +
+
+ + + + + {% set next_user_dir = 'asc' if sort != 'user' or dir == 'desc' else 'desc' %} + {% set next_platform_dir = 'asc' if sort != 'platform' or dir == 'desc' else 'desc' %} + {% set next_total_dir = 'asc' if sort != 'total_size' or dir == 'desc' else 'desc' %} + {% set next_count_dir = 'asc' if sort != 'video_count' or dir == 'desc' else 'desc' %} + {% set next_avg_dir = 'asc' if sort != 'avg_size' or dir == 'desc' else 'desc' %} + + + + + + + + + + {% for key, stats in storage_usage %} + {% set user, platform = key.split("::") %} + + + + + + + + {% endfor %} + +
+ + User{% if sort=='user' %} {{ '▲' if dir=='asc' else '▼' }}{% endif %} + + + + Platform{% if sort=='platform' %} {{ '▲' if dir=='asc' else '▼' }}{% endif %} + + + + Total Storage (GB){% if sort=='total_size' %} {{ '▲' if dir=='asc' else '▼' }}{% endif %} + + + + Video Count{% if sort=='video_count' %} {{ '▲' if dir=='asc' else '▼' }}{% endif %} + + + + Avg Size per Video (GB){% if sort=='avg_size' %} {{ '▲' if dir=='asc' else '▼' }}{% endif %} + +
{{ user }}{{ platform }}{{ "%.2f"|format(stats.total_size) }}{{ stats.video_count }}{{ "%.2f"|format(avg_sizes[key]) }}
+ + + {% if total_pages > 1 %} + + {% endif %} + + diff --git a/templates/recent.html b/templates/recent.html new file mode 100644 index 0000000..be816be --- /dev/null +++ b/templates/recent.html @@ -0,0 +1,67 @@ + + + + + Recent Videos + + + +

Recent Videos

+ +
+ {% for v in videos %} +
+ + thumb + +
+ +
+ by {{ v.username }} + · {{ v.platform }} +
+
+ {{ (v.created_at or v.updated_at) }} +
+
+ {{ "%.1f"|format((v.size or 0)|float) }} MB · {{ v.duration or 0 }}s +
+
+
+ {% endfor %} +
+ +
+ {% if page > 1 %} + « Prev + {% endif %} + Page {{ page }} / {{ total_pages }} + {% if page < total_pages %} + Next » + {% endif %} +
+ + diff --git a/templates/user_page.html b/templates/user_page.html index 27478df..1109130 100644 --- a/templates/user_page.html +++ b/templates/user_page.html @@ -39,16 +39,24 @@ padding: 10px; border-radius: 8px; transition: transform 0.2s ease; + position: relative; + /* <-- allow overlay star positioning */ } .video-card:hover { transform: scale(1.03); } + .thumb-link { + display: block; + position: relative; + } + .video-card img { width: 100%; border-radius: 5px; cursor: pointer; + display: block; } .video-size { @@ -57,6 +65,30 @@ color: #bbb; } + /* Favorite star */ + .fav-btn { + position: absolute; + top: 12px; + right: 12px; + font-size: 1.2rem; + line-height: 1; + border: none; + background: rgba(0, 0, 0, 0.45); + color: #fff; + cursor: pointer; + padding: .25rem .45rem; + border-radius: 6px; + backdrop-filter: blur(2px); + } + + .fav-btn[aria-pressed="true"] { + color: gold; + } + + .fav-btn:hover { + transform: scale(1.05); + } + .pagination { margin: 20px 0; } @@ -77,9 +109,19 @@
{% for video in videos %}
- - Thumbnail - +
+ + Thumbnail + + + + +
+

{{ "%.2f"|format(video.size/1024) }} GB

{% endfor %} @@ -90,10 +132,37 @@ ⬅ Prev {% endif %} Page {{ page }} / {{ total_pages }} - {% if page < total_pages %} - Next ➡ - {% endif %} + {% if page < total_pages %} Next ➡ + {% endif %}
+ + \ No newline at end of file diff --git a/templates/video_view.html b/templates/video_view.html index 3286ce5..51f4178 100644 --- a/templates/video_view.html +++ b/templates/video_view.html @@ -1,7 +1,8 @@ + - + {{ video.username }} - Video + + + + + + +

{{ video.username }} - {{ video.site }}

+ + + +
- +
-

Username: {{ video.username }}

+ + {{ video.username }} +

Size: {{ "%.2f"|format(video.size/1024) }} GB

Duration: {{ video.duration if video.duration else "Unknown" }}

Path: {{ video.filepath }}

-

Date: {{ video.createdAt }}

+

Date: {{ video.created_at }}

+ \ No newline at end of file