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.

374 lines
12 KiB
Python

2 months ago
from flask import Blueprint, render_template, request, send_file, jsonify
1 month ago
import math
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
1 month ago
from config import VIDEOS_PER_PAGE, DASHBOARD_PER_PAGE, get_local_db_connection
1 month ago
from datetime import date, datetime, timedelta
import requests
web = Blueprint("web", __name__)
def _get_recording_streamers() -> set[str]:
try:
1 month ago
resp = requests.get("http://127.0.0.1:5000/api/get_recording/")
resp.raise_for_status()
return resp.json()
except Exception:
return set()
2 months ago
def _parse_dates(timeframe: str, start_str: str | None, end_str: str | None):
"""Return (start, end) as date objects or (None, None). End inclusive by day."""
today = date.today()
if timeframe == "week":
return (today - timedelta(days=6)), today
if timeframe == "month":
return (today - timedelta(days=29)), today
if timeframe == "year":
return (today - timedelta(days=364)), today
if timeframe == "custom":
try:
s = datetime.strptime(start_str, "%Y-%m-%d").date() if start_str else None
e = datetime.strptime(end_str, "%Y-%m-%d").date() if end_str else None
if s and e and s > e: s, e = e, s
return s, e
except ValueError:
return None, None
return None, None
@web.route("/")
def dashboard():
2 months ago
# ---- read filters ----
query = request.args.get("q", "").lower().strip()
2 months ago
sort = request.args.get("sort", "total_size")
dir_ = request.args.get("dir", "desc")
reverse = (dir_ == "desc")
2 months ago
timeframe = request.args.get("timeframe", "all")
start_str = request.args.get("start")
end_str = request.args.get("end")
2 months ago
start, end = _parse_dates(timeframe, start_str, end_str)
# ---- build cache over timeframe ----
cache = build_cache(start=start, end=end)
1 month ago
items = list(cache["storage_usage"].items())
2 months ago
# ---- search ----
if query:
items = [e for e in items if query in e[0].split("::")[0].lower()]
# ---- sort keys ----
2 months ago
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]]
def k_last(x):
v = x[1].get("last_online")
# put None at the end when descending
return v if v is not None else (float("-inf") if not reverse else float("inf"))
key_map = {
"user": k_user,
"platform": k_platform,
"total_size": k_total,
"video_count": k_count,
"avg_size": k_avg,
"last_online": k_last, # <— new
}
base_key = key_map.get(sort, k_total)
# ---- get recording list → two sets: online + recording_offline ----
online_usernames: set[str] = set()
recording_offline_usernames: set[str] = set()
def user_of(item) -> str:
return item[0].split("::")[0]
def is_online(item) -> bool:
return user_of(item).lower() in online_usernames
def is_recording_offline(item) -> bool:
u = user_of(item).lower()
return (u in recording_offline_usernames) and (u not in online_usernames)
1 month ago
try:
rec_list = _get_recording_streamers()
for s in rec_list or []:
u = (s.get("username") or "").lower()
if not u:
continue
if s.get("is_online"):
online_usernames.add(u)
else:
recording_offline_usernames.add(u)
except Exception:
pass
# ---- sort with optional grouping ----
online_items = [x for x in items if is_online(x)]
recording_offline_items = [x for x in items if is_recording_offline(x)]
the_rest = [x for x in items if (x not in online_items) and (x not in recording_offline_items)]
1 month ago
online_items.sort(key=base_key, reverse=reverse)
recording_offline_items.sort(key=base_key, reverse=reverse)
the_rest.sort(key=base_key, reverse=reverse)
items = online_items + recording_offline_items + the_rest
2 months ago
# ---- paginate ----
page = max(1, int(request.args.get("page", 1)))
total_pages = max(1, math.ceil(len(items) / DASHBOARD_PER_PAGE))
2 months ago
start_idx = (page - 1) * DASHBOARD_PER_PAGE
paginated = items[start_idx:start_idx + 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,
2 months ago
dir=dir_,
timeframe=timeframe,
start_date=start_str,
end_date=end_str,
online_set=online_usernames,
recording_offline_set=recording_offline_usernames,
)
2 months ago
@web.route("/user/<username>")
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/<video_id>")
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/<video_id>")
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)))
1 month ago
videos, total = db_get_recent(page, VIDEOS_PER_PAGE)
total_pages = max(1, math.ceil(total / VIDEOS_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)))
1 month ago
videos, total = db_get_favorites(page, VIDEOS_PER_PAGE)
total_pages = max(1, math.ceil(total / VIDEOS_PER_PAGE))
mark_favorites(videos)
return render_template(
"favorites.html",
videos=videos,
page=page,
total_pages=total_pages
1 month ago
)
@web.route("/users")
def users():
# ---- filters ----
1 month ago
q = (request.args.get("q") or "").lower().strip()
sort = request.args.get("sort", "total_size") # user|site|total_size|video_count
1 month ago
dir_ = request.args.get("dir", "desc")
reverse = (dir_ == "desc")
timeframe = request.args.get("timeframe", "all")
start_str = request.args.get("start")
end_str = request.args.get("end")
show_online_first = request.args.get("online") == "1"
start, end = _parse_dates(timeframe, start_str, end_str)
# ---- WHERE ----
where = ["1=1"]
params = {}
if q:
where.append("LOWER(username) LIKE %(q)s")
params["q"] = f"%{q}%"
if start:
where.append("created_at >= %(start)s")
params["start"] = start
if end:
where.append("created_at < %(end)s")
params["end"] = end
where_sql = " AND ".join(where)
# ---- ORDER BY (use computed GB alias) ----
1 month ago
sort_map = {
"user": "username",
"site": "site",
"total_size": "total_gb", # <- sort by GB, not raw MB sum
1 month ago
"video_count": "video_count",
}
order_col = sort_map.get(sort, "total_gb")
1 month ago
order_dir = "DESC" if reverse else "ASC"
# ---- pagination ----
page = max(1, int(request.args.get("page", 1)))
per_page = 100
1 month ago
offset = (page - 1) * per_page
# ---- count distinct (username, site) for pager ----
count_sql = f"""
SELECT COUNT(*) AS n FROM (
SELECT 1
FROM videos
WHERE {where_sql}
GROUP BY username, site
) t;
"""
# size is in **MB**, convert to GB:
# 1 GiB = 1024 MB → divide by 1024.0
# (If you really want decimal GB, change 1024.0 to 1000.0)
1 month ago
agg_sql = f"""
SELECT
username,
site,
COUNT(*) AS video_count,
SUM(size) AS total_mb,
AVG(size) AS avg_mb,
(SUM(size)::numeric / 1024.0) AS total_gb,
(AVG(size)::numeric / 1024.0) AS avg_gb
1 month ago
FROM videos
WHERE {where_sql}
GROUP BY username, site
ORDER BY {order_col} {order_dir}
LIMIT %(limit)s OFFSET %(offset)s;
"""
params.update({"limit": per_page, "offset": offset})
conn, cur = get_local_db_connection()
cur = conn.cursor()
cur.execute(count_sql, params)
total_rows = cur.fetchone()[0] or 0
total_pages = max(1, math.ceil(total_rows / per_page))
cur.execute(agg_sql, params)
rows = cur.fetchall()
# rows: (username, site, video_count, total_mb, avg_mb, total_gb, avg_gb)
1 month ago
# ---- online/recording status sets (optional) ----
1 month ago
online_usernames: set[str] = set()
recording_offline_usernames: set[str] = set()
if show_online_first:
try:
rec_list = _get_recording_streamers()
for s in rec_list or []:
u = (s.get("username") or "").lower()
if not u:
continue
if s.get("is_online"):
online_usernames.add(u)
else:
recording_offline_usernames.add(u)
except Exception:
pass
# ---- thumbnail candidates per (user, site) ----
tcur = conn.cursor()
1 month ago
thumb_sql = """
SELECT thumbnail
FROM videos
WHERE username = %(u)s
AND site = %(s)s
AND thumbnail IS NOT NULL
AND thumbnail <> ''
1 month ago
ORDER BY created_at DESC
LIMIT 3;
"""
cards = []
for (username, site, video_count, total_mb, avg_mb, total_gb, avg_gb) in rows:
# fetch up to 3 recent thumbnails
1 month ago
thumb_urls = []
try:
tcur.execute(thumb_sql, {"u": username, "s": site})
1 month ago
thumb_urls = [r[0] for r in tcur.fetchall() if r and r[0]]
except Exception:
pass
total_gb_val = float(total_gb or 0.0)
avg_gb_val = float(avg_gb or 0.0)
1 month ago
uname_low = (username or "").lower()
cards.append({
"user": username,
"site": site,
"video_count": int(video_count),
# numeric
"total_size": total_gb_val,
"avg_size": avg_gb_val,
# preformatted strings for display
"total_size_display": f"{total_gb_val:.2f}",
"avg_size_display": f"{avg_gb_val:.2f}",
1 month ago
"thumb_urls": thumb_urls,
"is_online": uname_low in online_usernames,
"is_recording_offline": (uname_low in recording_offline_usernames) and (uname_low not in online_usernames),
})
if show_online_first:
online_cards = [c for c in cards if c["is_online"]]
rec_off_cards = [c for c in cards if c["is_recording_offline"] and not c["is_online"]]
the_rest = [c for c in cards if (c not in online_cards) and (c not in rec_off_cards)]
1 month ago
key_map = {
"user": lambda c: c["user"].lower(),
"site": lambda c: c["site"].lower(),
"total_size": lambda c: c["total_size"],
"video_count": lambda c: c["video_count"],
}
k = key_map.get(sort, key_map["total_size"])
online_cards.sort(key=k, reverse=reverse)
rec_off_cards.sort(key=k, reverse=reverse)
the_rest.sort(key=k, reverse=reverse)
cards = online_cards + rec_off_cards + the_rest
return render_template(
"users.html",
cards=cards,
page=page,
total_pages=total_pages,
query=q,
sort=sort,
dir=dir_,
timeframe=timeframe,
start_date=start_str,
end_date=end_str,
online="1" if show_online_first else "0",
)