time frame sort

main
oscar 2 months ago
parent 168fc40b40
commit 0c40cee26d

@ -1,10 +1,13 @@
import time
from helpers.db import db_get_videos
# from helpers.thumbnails import generate_thumbnails_for_videos # optional
# ───────── CACHE BUILDER ───────── #
def build_cache():
videos = db_get_videos()
def build_cache(start=None, end=None):
"""
Build storage usage cache over an optional date range.
start/end are date or datetime; end is inclusive by calendar day.
"""
videos = db_get_videos(start=start, end=end)
grouped = {}
for v in videos:
@ -16,24 +19,21 @@ def build_cache():
for (username, platform), vids in grouped.items():
key = f"{username}::{platform}"
total_gb = 0
total_gb = 0.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')}")
total_gb += float(v.get("size", 0) or 0) / 1024.0
except (ValueError, TypeError):
# ignore bad rows
continue
storage_usage[key] = {
"total_size": total_gb,
"video_count": len(vids)
}
avg_sizes[key] = total_gb / len(vids) if vids else 0
avg_sizes[key] = (total_gb / len(vids)) if vids else 0.0
video_map[key] = vids
# Thumbnail generation is optional, uncomment if you want it auto-built:
# generate_thumbnails_for_videos(videos)
return {
"timestamp": time.time(),
"videos": video_map,

@ -1,8 +1,13 @@
import psycopg2.extras
from config import get_local_db_connection # central config
from datetime import datetime, timedelta
from config import get_local_db_connection
# ───────── DB HELPER ───────── #
def db_get_videos(username: str = None):
def db_get_videos(username: str = None, start=None, end=None):
"""
Fetch videos, optionally filtered by username and created_at date range.
`start` / `end` can be date or datetime (UTC). End is inclusive by day.
"""
conn, cur = get_local_db_connection()
cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor)
@ -19,6 +24,21 @@ def db_get_videos(username: str = None):
query += " AND username = %s"
params.append(username)
if start is not None:
# Normalize to datetime midnight if date
if hasattr(start, "hour") is False:
start = datetime.combine(start, datetime.min.time())
query += " AND created_at >= %s"
params.append(start)
if end is not None:
# Make end inclusive by bumping 1 day and using '<'
if hasattr(end, "hour") is False:
end = datetime.combine(end, datetime.min.time())
end_exclusive = end + timedelta(days=1)
query += " AND created_at < %s"
params.append(end_exclusive)
query += " ORDER BY created_at DESC"
cur.execute(query, params)

@ -7,19 +7,52 @@ from config import VIDEOS_PER_PAGE, DASHBOARD_PER_PAGE
web = Blueprint("web", __name__)
from datetime import date, datetime, timedelta
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:
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
# "all" or unknown → no filter
return None, None
@web.route("/")
def dashboard():
cache = build_cache()
# ---- read filters ----
query = request.args.get("q", "").lower().strip()
sort = request.args.get("sort", "total_size")
dir_ = request.args.get("dir", "desc")
timeframe = request.args.get("timeframe", "all")
start_str = request.args.get("start")
end_str = request.args.get("end")
reverse = (dir_ == "desc")
start, end = _parse_dates(timeframe, start_str, end_str)
# ---- build cache over timeframe ----
cache = build_cache(start=start, end=end)
items = list(cache["storage_usage"].items())
# ---- search ----
if query:
items = [e for e in items if query in e[0].split("::")[0].lower()]
# ---- sort ----
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"]
@ -35,10 +68,11 @@ def dashboard():
}
items.sort(key=key_map.get(sort, k_total), reverse=reverse)
# ---- paginate ----
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]
start_idx = (page - 1) * DASHBOARD_PER_PAGE
paginated = items[start_idx:start_idx + DASHBOARD_PER_PAGE]
return render_template(
"main.html",
@ -48,7 +82,10 @@ def dashboard():
total_pages=total_pages,
query=query,
sort=sort,
dir=dir_
dir=dir_,
timeframe=timeframe,
start_date=start_str,
end_date=end_str
)
@web.route("/refresh")

@ -1,80 +1,148 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>📊 Video Storage Analytics</title>
<style>
body { font-family: Arial, sans-serif; background:#111; color:#eee; text-align:center; }
table { margin:auto; border-collapse:collapse; width:90%; margin-top:20px; }
th, td { border:1px solid #444; padding:10px; }
th { background:#333; }
tr:nth-child(even){ background:#222; }
.controls { margin: 10px 0; }
.pagination { margin-top: 15px; }
.pagination a, .pagination span {
display:inline-block; background:#222; color:#eee; border:1px solid #444;
margin:0 5px; padding:6px 12px; text-decoration:none;
body {
font-family: Arial, sans-serif;
background: #111;
color: #eee;
text-align: center;
}
table {
margin: auto;
border-collapse: collapse;
width: 90%;
margin-top: 20px;
}
th,
td {
border: 1px solid #444;
padding: 10px;
}
th {
background: #333;
}
tr:nth-child(even) {
background: #222;
}
.controls {
margin: 10px 0;
}
.pagination {
margin-top: 15px;
}
.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;
}
input[type="text"] {
padding: 8px;
width: 300px;
}
button {
padding: 8px 12px;
}
.pagination .active { background:#555; }
input[type="text"] { padding:8px; width:300px; }
button { padding:8px 12px; }
</style>
</head>
<body>
<script>
const table = document.getElementById('analytics-table');
const headers = table.querySelectorAll('th');
const searchInput = document.getElementById('search');
const rows = Array.from(table.querySelector('tbody').rows);
const pagination = document.getElementById('pagination');
let sortDirection = {};
let currentPage = 1;
const rowsPerPage = 100;
// Sorting
headers.forEach((header, index) => {
header.addEventListener('click', () => {
const isNumeric = index >= 2;
const dir = sortDirection[index] === 'asc' ? 'desc' : 'asc';
sortDirection = { [index]: dir };
headers.forEach(h => h.classList.remove('sort-asc', 'sort-desc'));
header.classList.add(dir === 'asc' ? 'sort-asc' : 'sort-desc');
rows.sort((a, b) => {
const aVal = isNumeric ? parseFloat(a.cells[index].innerText) : a.cells[index].innerText.toLowerCase();
const bVal = isNumeric ? parseFloat(b.cells[index].innerText) : b.cells[index].innerText.toLowerCase();
return dir === 'asc' ? (aVal > bVal ? 1 : -1) : (aVal < bVal ? 1 : -1);
});
updateTable();
});
<script>
const table = document.getElementById('analytics-table');
const headers = table.querySelectorAll('th');
const searchInput = document.getElementById('search');
const rows = Array.from(table.querySelector('tbody').rows);
const pagination = document.getElementById('pagination');
let sortDirection = {};
let currentPage = 1;
const rowsPerPage = 100;
// Sorting
headers.forEach((header, index) => {
header.addEventListener('click', () => {
const isNumeric = index >= 2;
const dir = sortDirection[index] === 'asc' ? 'desc' : 'asc';
sortDirection = { [index]: dir };
headers.forEach(h => h.classList.remove('sort-asc', 'sort-desc'));
header.classList.add(dir === 'asc' ? 'sort-asc' : 'sort-desc');
rows.sort((a, b) => {
const aVal = isNumeric ? parseFloat(a.cells[index].innerText) : a.cells[index].innerText.toLowerCase();
const bVal = isNumeric ? parseFloat(b.cells[index].innerText) : b.cells[index].innerText.toLowerCase();
return dir === 'asc' ? (aVal > bVal ? 1 : -1) : (aVal < bVal ? 1 : -1);
});
updateTable();
});
});
// Search
searchInput.addEventListener('keyup', () => {
currentPage = 1;
updateTable();
});
// Search
searchInput.addEventListener('keyup', () => {
currentPage = 1;
updateTable();
});
function updateTable() {
const term = searchInput.value.toLowerCase();
const filtered = rows.filter(row => row.cells[0].innerText.toLowerCase().includes(term));
function updateTable() {
const term = searchInput.value.toLowerCase();
const filtered = rows.filter(row => row.cells[0].innerText.toLowerCase().includes(term));
const totalPages = Math.ceil(filtered.length / rowsPerPage);
updatePagination(totalPages);
const totalPages = Math.ceil(filtered.length / rowsPerPage);
updatePagination(totalPages);
table.querySelector('tbody').innerHTML = '';
const start = (currentPage - 1) * rowsPerPage;
const paginated = filtered.slice(start, start + rowsPerPage);
table.querySelector('tbody').innerHTML = '';
const start = (currentPage - 1) * rowsPerPage;
const paginated = filtered.slice(start, start + rowsPerPage);
paginated.forEach(row => table.querySelector('tbody').appendChild(row));
}
paginated.forEach(row => table.querySelector('tbody').appendChild(row));
}
updateTable(); // Initial render
</script>
updateTable(); // Initial render
</script>
<h1>📊 Video Storage Analytics</h1>
<div class="controls">
<form method="get" action="{{ url_for('web.dashboard') }}"
style="display:inline-flex; gap:8px; align-items:center; margin-left:10px;">
<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 }}">
<button type="submit">Apply</button>
</form>
<button onclick="window.location.href='/refresh'">🔄 Refresh Data</button>
<!-- Server-side search -->
@ -86,7 +154,7 @@
<table id="analytics-table">
<thead>
<tr>
<tr>
{% 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' %}
@ -94,69 +162,69 @@
{% set next_avg_dir = 'asc' if sort != 'avg_size' or dir == 'desc' else 'desc' %}
<th>
<a href="{{ url_for('web.dashboard', q=query, page=1, sort='user', dir=next_user_dir) }}">
<a href="{{ url_for('web.dashboard', q=query, page=1, sort='user', dir=next_user_dir) }}">
User{% if sort=='user' %} {{ '▲' if dir=='asc' else '▼' }}{% endif %}
</a>
</a>
</th>
<th>
<a href="{{ url_for('web.dashboard', q=query, page=1, sort='platform', dir=next_platform_dir) }}">
<a href="{{ url_for('web.dashboard', q=query, page=1, sort='platform', dir=next_platform_dir) }}">
Platform{% if sort=='platform' %} {{ '▲' if dir=='asc' else '▼' }}{% endif %}
</a>
</a>
</th>
<th>
<a href="{{ url_for('web.dashboard', q=query, page=1, sort='total_size', dir=next_total_dir) }}">
<a href="{{ url_for('web.dashboard', q=query, page=1, sort='total_size', dir=next_total_dir) }}">
Total Storage (GB){% if sort=='total_size' %} {{ '▲' if dir=='asc' else '▼' }}{% endif %}
</a>
</a>
</th>
<th>
<a href="{{ url_for('web.dashboard', q=query, page=1, sort='video_count', dir=next_count_dir) }}">
<a href="{{ url_for('web.dashboard', q=query, page=1, sort='video_count', dir=next_count_dir) }}">
Video Count{% if sort=='video_count' %} {{ '▲' if dir=='asc' else '▼' }}{% endif %}
</a>
</a>
</th>
<th>
<a href="{{ url_for('web.dashboard', q=query, page=1, sort='avg_size', dir=next_avg_dir) }}">
<a href="{{ url_for('web.dashboard', q=query, page=1, sort='avg_size', dir=next_avg_dir) }}">
Avg Size per Video (GB){% if sort=='avg_size' %} {{ '▲' if dir=='asc' else '▼' }}{% endif %}
</a>
</a>
</th>
</tr>
</tr>
</thead>
<tbody>
{% for key, stats in storage_usage %}
{% set user, platform = key.split("::") %}
<tr>
<td><a href="/user/{{ user }}" style="color:#4af;">{{ user }}</a></td>
<td>{{ platform }}</td>
<td>{{ "%.2f"|format(stats.total_size) }}</td>
<td>{{ stats.video_count }}</td>
<td>{{ "%.2f"|format(avg_sizes[key]) }}</td>
</tr>
{% set user, platform = key.split("::") %}
<tr>
<td><a href="/user/{{ user }}" style="color:#4af;">{{ user }}</a></td>
<td>{{ platform }}</td>
<td>{{ "%.2f"|format(stats.total_size) }}</td>
<td>{{ stats.video_count }}</td>
<td>{{ "%.2f"|format(avg_sizes[key]) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<!-- Server-side pagination -->
{% if total_pages > 1 %}
<div class="pagination">
{% if page > 1 %}
<a href="{{ url_for('web.dashboard', page=page-1, q=query) }}">« 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.dashboard', page=p, q=query) }}">{{ p }}</a>
{% endif %}
{% endfor %}
{% if page < total_pages %}
<a href="{{ url_for('web.dashboard', page=page+1, q=query) }}">Next »</a>
<div class="pagination">
{% if page > 1 %}
<a href="{{ url_for('web.dashboard', page=page-1, q=query) }}">« 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.dashboard', page=p, q=query) }}">{{ p }}</a>
{% endif %}
{% endfor %}
{% if page < total_pages %} <a href="{{ url_for('web.dashboard', page=page+1, q=query) }}">Next »</a>
{% else %}
<span>Next »</span>
<span>Next »</span>
{% endif %}
</div>
</div>
{% endif %}
</body>
</html>
Loading…
Cancel
Save