main
oscar 3 months ago
parent 40f8ce8e2c
commit 500129812f

@ -1,9 +1,6 @@
# app.py Optimised AF
from flask import Flask, render_template, request, redirect, url_for, jsonify
from funcs import (
process_videos, group_videos, match_data_to_video_fast,
get_all_videos, get_all_data
)
from flask import Flask, render_template, request, redirect, url_for, jsonify, send_from_directory, send_file
from funcs import process_videos, group_videos, match_data_to_video_fast, get_all_videos, get_all_data
from config import connect_redis
from concurrent.futures import ThreadPoolExecutor
import hashlib, json, math, os, subprocess, time, zlib
@ -15,8 +12,9 @@ redis = connect_redis()
CACHE_KEY = "video_cache_v2" # bump key so we dont fight old data
META_HASH = "video_meta_v2" # per-file meta cache
THUMB_DIR = "static/thumbnails"
VIDEOS_PER_PAGE = 20
THUMB_WIDTH = 320 # px
VIDEOS_PER_PAGE = 40
DASHBOARD_PER_PAGE = 100 # for the dashboard
THUMB_WIDTH = 640 # px
FF_QUALITY = "80" # 0-100 for WebP
SCAN_DIRS = [
@ -136,15 +134,27 @@ def get_cached_data():
@app.route("/")
def dashboard():
cache = get_cached_data()
sorted_usage = sorted(
cache["storage_usage"].items(),
key=lambda x: x[1]["total_size"],
reverse=True
)
# --- SEARCH ---
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 = [entry for entry in sorted_usage if query in entry[0].lower()]
# --- PAGINATION ---
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_usage = sorted_usage[start:start + DASHBOARD_PER_PAGE]
return render_template(
"analytics.html",
storage_usage=sorted_usage,
avg_sizes=cache["avg_sizes"]
storage_usage=paginated_usage,
avg_sizes=cache["avg_sizes"],
page=page,
total_pages=total_pages,
query=query
)
@app.route("/refresh")
@ -158,21 +168,52 @@ def refresh():
@app.route("/user/<username>")
def user_page(username):
cache = get_cached_data()
cache = get_cached_data()
videos = [v | {"platform": key.split("::")[1]}
for key, vids in cache["videos"].items()
if key.split("::")[0] == username
for v in vids]
# generate video_id for sorting
for v in videos:
v["video_id"] = os.path.basename(v["filepath"]).rsplit(".", 1)[0]
# Pagination
page = max(1, int(request.args.get("page", 1)))
per_page = VIDEOS_PER_PAGE
total_pages = max(1, math.ceil(len(videos) / per_page))
start = (page - 1) * per_page
paginated_videos = videos[start:start + per_page]
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
return render_template(
"user_page.html",
username=username,
videos=videos[start:start + VIDEOS_PER_PAGE],
page=page, total_pages=total_pages
videos=paginated_videos,
page=page,
total_pages=total_pages
)
@app.route("/video/stream/<video_id>")
def stream_video(video_id):
cache = get_cached_data()
for vids in cache["videos"].values():
for v in vids:
vid_id = os.path.basename(v["filepath"]).rsplit(".", 1)[0]
if vid_id == video_id:
return send_file(v["filepath"], mimetype="video/mp4")
return "Video not found", 404
@app.route("/video/<video_id>")
def view_video(video_id):
cache = get_cached_data()
for vids in cache["videos"].values():
for v in vids:
vid_id = os.path.splitext(os.path.basename(v["filepath"]))[0]
if vid_id == video_id:
v["video_id"] = vid_id # ✅ precompute safe ID
return render_template("video_view.html", video=v)
return "Video not found", 404
if __name__ == "__main__":
app.run(debug=True)

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<title>Video Storage Analytics</title>
<title>📊 Video Storage Analytics</title>
<style>
body {
font-family: Arial, sans-serif;
@ -47,6 +47,23 @@
padding: 8px;
width: 300px;
}
.pagination {
margin-top: 15px;
}
.pagination button {
background: #222;
color: #eee;
border: 1px solid #444;
margin: 0 5px;
padding: 6px 12px;
cursor: pointer;
}
.pagination button.active {
background: #555;
}
</style>
</head>
@ -54,6 +71,7 @@
<h1>📊 Video Storage Analytics</h1>
<button onclick="window.location.href='/refresh'">🔄 Refresh Data</button>
<input type="text" id="search" placeholder="Search users...">
<table id="analytics-table">
<thead>
<tr>
@ -68,7 +86,7 @@
{% for key, stats in storage_usage %}
{% set user, platform = key.split("::") %}
<tr>
<td><a href="/user/{{ user }}">{{ user }}</a></td>
<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>
@ -77,18 +95,25 @@
{% endfor %}
</tbody>
</table>
<div class="pagination" id="pagination"></div>
<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 rows = Array.from(table.querySelector('tbody').rows);
const isNumeric = index >= 2;
const dir = sortDirection[index] === 'asc' ? 'desc' : 'asc';
sortDirection[index] = dir;
sortDirection = { [index]: dir };
headers.forEach(h => h.classList.remove('sort-asc', 'sort-desc'));
header.classList.add(dir === 'asc' ? 'sort-asc' : 'sort-desc');
@ -97,17 +122,46 @@
const bVal = isNumeric ? parseFloat(b.cells[index].innerText) : b.cells[index].innerText.toLowerCase();
return dir === 'asc' ? (aVal > bVal ? 1 : -1) : (aVal < bVal ? 1 : -1);
});
rows.forEach(row => table.querySelector('tbody').appendChild(row));
updateTable();
});
});
// Search
searchInput.addEventListener('keyup', () => {
const term = searchInput.value.toLowerCase();
Array.from(table.querySelector('tbody').rows).forEach(row => {
const text = row.cells[0].innerText.toLowerCase();
row.style.display = text.includes(term) ? '' : 'none';
});
currentPage = 1;
updateTable();
});
// Pagination
function updatePagination(totalPages) {
pagination.innerHTML = '';
for (let i = 1; i <= totalPages; i++) {
const btn = document.createElement('button');
btn.innerText = i;
btn.classList.toggle('active', i === currentPage);
btn.addEventListener('click', () => {
currentPage = i;
updateTable();
});
pagination.appendChild(btn);
}
}
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);
table.querySelector('tbody').innerHTML = '';
const start = (currentPage - 1) * rowsPerPage;
const paginated = filtered.slice(start, start + rowsPerPage);
paginated.forEach(row => table.querySelector('tbody').appendChild(row));
}
updateTable(); // Initial render
</script>
</body>

@ -10,57 +10,89 @@
background: #111;
color: #eee;
text-align: center;
margin: 0;
padding: 0;
}
h1 {
margin-top: 20px;
}
.back-link {
display: inline-block;
margin: 10px;
color: #0af;
text-decoration: none;
font-size: 16px;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 18px;
margin: 20px;
padding: 0 20px;
}
.video {
.video-card {
background: #222;
padding: 10px;
border-radius: 5px;
border-radius: 8px;
transition: transform 0.2s ease;
}
img {
.video-card:hover {
transform: scale(1.03);
}
.video-card img {
width: 100%;
border-radius: 5px;
cursor: pointer;
}
.video-size {
margin-top: 8px;
font-size: 14px;
color: #bbb;
}
.pagination {
margin-top: 20px;
margin: 20px 0;
}
a {
.pagination a {
color: #0af;
margin: 0 8px;
text-decoration: none;
font-size: 16px;
}
</style>
</head>
<body>
<h1>🎥 Videos for {{ username }}</h1>
<a href="/">⬅ Back to Dashboard</a>
<a href="/" class="back-link">⬅ Back to Dashboard</a>
<div class="grid">
{% for video in videos %}
<div class="video">
<img src="/{{ video.thumbnail }}" alt="Thumbnail">
<p><b>{{ video['filepath'].split('/')[-1] }}</b></p>
<p>Platform: {{ video.platform }}</p>
<p>Size: {{ "%.2f"|format(video.size/1024) }} GB</p>
<div class="video-card">
<a href="/video/{{ video['video_id'] }}">
<img src="/{{ video.thumbnail }}" alt="Thumbnail">
</a>
<p class="video-size">{{ "%.2f"|format(video.size/1024) }} GB</p>
</div>
{% endfor %}
</div>
<div class="pagination">
{% if page > 1 %}
<a href="?page={{ page-1 }}">⬅ Prev</a>
{% endif %}
<span>Page {{ page }} / {{ total_pages }}</span>
{% if page < total_pages %} <a href="?page={{ page+1 }}">Next ➡</a>
{% endif %}
{% if page < total_pages %}
<a href="?page={{ page+1 }}">Next ➡</a>
{% endif %}
</div>
</body>

@ -0,0 +1,52 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{ video.username }} - Video</title>
<style>
body {
font-family: Arial, sans-serif;
background: #111;
color: #eee;
text-align: center;
margin: 0;
padding: 0;
}
video {
margin-top: 20px;
width: 80%;
max-width: 1000px;
border: 2px solid #444;
border-radius: 8px;
background: #000;
}
.meta {
margin: 20px auto;
text-align: left;
max-width: 800px;
background: #222;
padding: 15px;
border-radius: 6px;
}
.meta p { margin: 8px 0; }
a { color: #0af; text-decoration: none; }
</style>
</head>
<body>
<h1>{{ video.username }} - {{ video.site }}</h1>
<a href="/user/{{ video.username }}">⬅ Back to {{ video.username }}'s Videos</a>
<video controls muted>
<source src="/video/stream/{{ video.video_id }}" type="video/mp4">
Your browser does not support the video tag.
</video>
<div class="meta">
<p><strong>Username:</strong> {{ video.username }}</p>
<p><strong>Size:</strong> {{ "%.2f"|format(video.size/1024) }} GB</p>
<p><strong>Duration:</strong> {{ video.duration if video.duration else "Unknown" }}</p>
<p><strong>Path:</strong> {{ video.filepath }}</p>
<p><strong>Date:</strong> {{ video.createdAt }}</p>
</div>
</body>
</html>
Loading…
Cancel
Save