users index page

main
oscar 1 month ago
parent 5c3f8dbd6a
commit 834ca1b272

@ -3,7 +3,7 @@ import math, time
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
from config import VIDEOS_PER_PAGE, DASHBOARD_PER_PAGE
from config import VIDEOS_PER_PAGE, DASHBOARD_PER_PAGE, get_local_db_connection
web = Blueprint("web", __name__)
@ -21,13 +21,10 @@ 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":
# This week = last 7 days ending today
return (today - timedelta(days=6)), today
if timeframe == "month":
# Last 30 days
return (today - timedelta(days=29)), today
if timeframe == "year":
# Last 365 days
return (today - timedelta(days=364)), today
if timeframe == "custom":
try:
@ -37,7 +34,6 @@ def _parse_dates(timeframe: str, start_str: str | None, end_str: str | None):
return s, e
except ValueError:
return None, None
# "all" or unknown → no filter
return None, None
@web.route("/")
@ -188,9 +184,8 @@ def view_video(video_id):
@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))
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(
@ -203,9 +198,8 @@ def recent():
@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))
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(
@ -213,4 +207,190 @@ def favorites_page():
videos=videos,
page=page,
total_pages=total_pages
)
)
@web.route("/users")
def users():
# ---- filters ----
q = (request.args.get("q") or "").lower().strip()
sort = request.args.get("sort", "total_size") # user|site|total_size|video_count
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 ----
sort_map = {
"user": "username",
"site": "site",
"total_size": "total_bytes",
"video_count": "video_count",
}
order_col = sort_map.get(sort, "total_bytes")
order_dir = "DESC" if reverse else "ASC"
# ---- pagination ----
page = max(1, int(request.args.get("page", 1)))
per_page = 100 # or your DASHBOARD_PER_PAGE
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;
"""
# ---- aggregate page ----
agg_sql = f"""
SELECT
username,
site,
COUNT(*) AS video_count,
SUM(size) AS total_bytes,
AVG(size) AS avg_bytes,
SUM(size)::numeric / 1000000000.0 AS total_gb,
AVG(size)::numeric / 1000000000.0 AS avg_gb
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})
from config import get_local_db_connection
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_bytes, avg_bytes)
# ---- get recording sets (for status dots) ----
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 subquery (only for current page) ----
thumb_sql = """
SELECT thumbnail
FROM videos
WHERE username = %(u)s
AND site = %(s)s
AND thumbnail IS NOT NULL
AND thumbnail <> ''
ORDER BY created_at DESC
LIMIT 3;
"""
tcur = conn.cursor()
def to_gb(n): return (n or 0) / 1_000_000_000.0
cards = []
for (username, site, video_count, total_bytes, avg_bytes, total_gb, avg_gb) in rows:
# fetch up to 3 recent thumbnails (unchanged logic you already added)
thumb_urls = []
try:
tcur.execute(
"""
SELECT thumbnail
FROM videos
WHERE username = %(u)s AND site = %(s)s
AND thumbnail IS NOT NULL AND thumbnail <> ''
ORDER BY created_at DESC
LIMIT 3;
""",
{"u": username, "s": site},
)
thumb_urls = [r[0] for r in tcur.fetchall() if r and r[0]]
except Exception:
pass
# ---- PRE-FORMAT display strings here (avoid Jinja float filter entirely) ----
total_gb_val = float(total_gb or 0)
avg_gb_val = float(avg_gb or 0)
uname_low = (username or "").lower()
cards.append({
"user": username,
"site": site,
"total_size": total_gb_val, # keep the raw number if you need it
"avg_size": avg_gb_val, # keep raw
"total_size_display": f"{total_gb_val:.2f}", # <— use this in HTML
"avg_size_display": f"{avg_gb_val:.2f}", # <— use this in HTML
"video_count": int(video_count),
"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),
})
# ---- optional: reorder with online-first grouping ----
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)]
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
# ---- render users.html ----
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",
)

@ -0,0 +1,178 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>👥 Users</title>
<style>
body { font-family: Arial, sans-serif; background:#111; color:#eee; }
a { color:#4af; text-decoration:none; }
.header { display:flex; gap:12px; align-items:center; justify-content:center; margin:18px 0; flex-wrap:wrap; }
input[type="text"] { padding:8px; width:280px; }
select, button { padding:8px 12px; background:#222; color:#eee; border:1px solid #444; }
.grid { width:92%; margin:0 auto; display:grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap:14px; }
/* Make thumb take full card height */
.card {
background:#1a1a1a; border:1px solid #333; border-radius:8px; padding:10px;
display:grid; grid-template-columns: 180px 1fr; gap:12px; align-items:stretch;
min-height: 120px;
}
.thumb {
position: relative;
width: 100%;
height: 100%;
min-height: 120px;
background:#222; border:1px solid #333; border-radius:6px; overflow:hidden;
}
.thumb img, .thumb .fallback {
position:absolute; inset:0;
width:100%; height:100%;
object-fit:cover; display:block;
}
.thumb .fallback { display:none; align-items:center; justify-content:center; font-size:28px; }
.meta { display:flex; flex-direction:column; gap:6px; }
.meta h3 { margin:0; font-size:16px; display:flex; align-items:center; gap:6px; }
.muted { color:#bbb; }
.row { font-size:14px; }
.status-dot { display:inline-block; width:10px; height:10px; border-radius:50%; vertical-align:middle; }
.dot-online { background:#22c55e; }
.dot-record { background:#ef4444; }
.dot-offline { background:#666; }
.pagination { margin:16px 0; text-align:center; }
.pagination a, .pagination span { display:inline-block; background:#222; color:#eee; border:1px solid #444; margin:0 5px; padding:6px 12px; text-decoration:none; }
.pagination .active { background:#555; }
.toolbar { display:flex; gap:10px; align-items:center; justify-content:center; flex-wrap:wrap; }
</style>
<script>
// If an <img> fails, hide it and show the next candidate; if none left, show the fallback emoji.
function tryNext(el) {
el.style.display = 'none';
let sib = el.nextElementSibling;
while (sib && sib.tagName !== 'IMG' && !(sib.classList && sib.classList.contains('fallback'))) {
sib = sib.nextElementSibling;
}
if (!sib) return;
if (sib.tagName === 'IMG') {
sib.style.display = 'block';
} else {
sib.style.display = 'flex';
}
}
</script>
</head>
<body>
<h1>👥 Users</h1>
<div class="header">
<!-- Search -->
<form method="get" action="{{ url_for('web.users') }}" class="toolbar">
<input type="text" name="q" placeholder="Search users..." value="{{ query or '' }}">
<input type="hidden" name="sort" value="{{ sort }}">
<input type="hidden" name="dir" value="{{ dir }}">
<input type="hidden" name="timeframe" value="{{ timeframe }}">
<input type="hidden" name="start" value="{{ start_date or '' }}">
<input type="hidden" name="end" value="{{ end_date or '' }}">
<button type="submit">Search</button>
</form>
<!-- Sort -->
{% set next_user_dir = 'asc' if sort != 'user' or dir == 'desc' else 'desc' %}
{% set next_site_dir = 'asc' if sort != 'site' or dir == 'desc' else 'desc' %}
{% set next_total_dir = 'asc' if sort != 'total_size_display' or dir == 'desc' else 'desc' %}
{% set next_count_dir = 'asc' if sort != 'video_count' or dir == 'desc' else 'desc' %}
<div class="toolbar">
<a href="{{ url_for('web.users', q=query, page=1, sort='user', dir=next_user_dir, online=online, timeframe=timeframe, start=start_date, end=end_date) }}">Sort: User{% if sort=='user' %} {{ '▲' if dir=='asc' else '▼' }}{% endif %}</a>
<a href="{{ url_for('web.users', q=query, page=1, sort='site', dir=next_site_dir, online=online, timeframe=timeframe, start=start_date, end=end_date) }}">Sort: Site{% if sort=='site' %} {{ '▲' if dir=='asc' else '▼' }}{% endif %}</a>
<a href="{{ url_for('web.users', q=query, page=1, sort='total_size_display', dir=next_total_dir, online=online, timeframe=timeframe, start=start_date, end=end_date) }}">Sort: Total Size{% if sort=='total_size_display' %} {{ '▲' if dir=='asc' else '▼' }}{% endif %}</a>
<a href="{{ url_for('web.users', q=query, page=1, sort='video_count', dir=next_count_dir, online=online, timeframe=timeframe, start=start_date, end=end_date) }}">Sort: Videos{% if sort=='video_count' %} {{ '▲' if dir=='asc' else '▼' }}{% endif %}</a>
<!-- Online-first toggle -->
<a href="{{ url_for('web.users', q=query, page=1, sort=sort, dir=dir, online=('0' if online=='1' else '1'), timeframe=timeframe, start=start_date, end=end_date) }}">
{{ '📶 Show online (ON)' if online=='1' else '📶 Show online (OFF)' }}
</a>
</div>
<!-- Timeframe -->
<form method="get" action="{{ url_for('web.users') }}" class="toolbar">
<label>Timeframe:</label>
<select name="timeframe" onchange="this.form.submit()">
<option value="all" {{ 'selected' if timeframe=='all' else '' }}>All time</option>
<option value="week" {{ 'selected' if timeframe=='week' else '' }}>This week</option>
<option value="month" {{ 'selected' if timeframe=='month' else '' }}>Last 30 days</option>
<option value="year" {{ 'selected' if timeframe=='year' else '' }}>Last 365 days</option>
<option value="custom"{{ 'selected' if timeframe=='custom' else '' }}>Custom…</option>
</select>
<input type="date" name="start" value="{{ start_date or '' }}" placeholder="Start" />
<input type="date" name="end" value="{{ end_date or '' }}" placeholder="End" />
<input type="hidden" name="q" value="{{ query or '' }}">
<input type="hidden" name="sort" value="{{ sort }}">
<input type="hidden" name="dir" value="{{ dir }}">
<input type="hidden" name="online" value="{{ online }}">
<button type="submit">Apply</button>
</form>
</div>
<div class="grid">
{% for c in cards %}
<div class="card">
<div class="thumb">
{% if c.thumb_urls and c.thumb_urls|length %}
{# render all candidates; show first, hide the rest; each tries the next on error #}
{% for url in c.thumb_urls %}
<img src="{{ url }}" loading="lazy" decoding="async" alt="{{ c.user }}" {% if not loop.first %}style="display:none"{% endif %} onerror="tryNext(this)">
{% endfor %}
{% endif %}
<span class="fallback">🎞️</span>
</div>
<div class="meta">
<h3>
<a href="{{ url_for('web.user_page', username=c.user) }}">{{ c.user }}</a>
{% if c.is_online %}
<span class="status-dot dot-online" title="Online"></span>
{% elif c.is_recording_offline %}
<span class="status-dot dot-record" title="Recording (offline)"></span>
{% else %}
<span class="status-dot dot-offline" title="Offline"></span>
{% endif %}
</h3>
<div class="row">
<span class="muted">Site:</span>
<a href="https://{{ c.site }}.com/{{ c.user }}" target="_blank" rel="noopener">{{ c.site }}</a>
</div>
<div class="row"><span class="muted">Total size:</span> {{ c.total_size_display }} GB</div>
<div class="row"><span class="muted">Videos:</span> {{ c.video_count }}</div>
</div>
</div>
{% endfor %}
</div>
{% if total_pages > 1 %}
<div class="pagination">
{% if page > 1 %}
<a href="{{ url_for('web.users', page=page-1, q=query, sort=sort, dir=dir, online=online, timeframe=timeframe, start=start_date, end=end_date) }}">« Prev</a>
{% else %}<span>« Prev</span>{% endif %}
{% for p in range(1, total_pages + 1) %}
{% if p == page %}
<span class="active">{{ p }}</span>
{% else %}
<a href="{{ url_for('web.users', page=p, q=query, sort=sort, dir=dir, online=online, timeframe=timeframe, start=start_date, end=end_date) }}">{{ p }}</a>
{% endif %}
{% endfor %}
{% if page < total_pages %}
<a href="{{ url_for('web.users', page=page+1, q=query, sort=sort, dir=dir, online=online, timeframe=timeframe, start=start_date, end=end_date) }}">Next »</a>
{% else %}<span>Next »</span>{% endif %}
</div>
{% endif %}
</body>
</html>
Loading…
Cancel
Save