Add app auth, dashboard, scheduler, video management, and new scrapers

- JWT-based app authentication with user roles, folder/route access control
- Dashboard with storage stats, health checks, and recent activity
- Auto-download/scrape scheduler (12h interval) with per-user and per-job configs
- Video upload, tagging, HLS transcoding, and detail pages
- New scrapers: LeakGallery, Mega (megajs), yt-dlp
- FlareSolverr integration for Cloudflare-protected sites
- Gallery: advanced filtering (date, size, search), sort modes, equal-mix shuffle
- Forum sites management with stored cookies/auth
- GridWall/GridCell components for responsive media grid
- Media API with folder-access permissions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey T
2026-04-16 07:48:10 -05:00
parent 4903b84aef
commit 236f36aae6
54 changed files with 9986 additions and 420 deletions
+38 -4
View File
@@ -1,5 +1,5 @@
import { useState, useEffect, useRef } from 'react'
import { getDownloadHistory, getActiveDownloads, getUser, getScrapeJobs } from '../api'
import { getDownloadHistory, getActiveDownloadDetails, getUser, getScrapeJobs } from '../api'
import Spinner from '../components/Spinner'
export default function Downloads() {
@@ -10,6 +10,8 @@ export default function Downloads() {
const [error, setError] = useState(null)
const pollRef = useRef(null)
const [usernames, setUsernames] = useState({})
const prevCompleted = useRef({})
const [speeds, setSpeeds] = useState({})
useEffect(() => {
loadAll()
@@ -26,7 +28,7 @@ export default function Downloads() {
const [histData, activeData, scrapeData] = await Promise.all([
getDownloadHistory(),
getActiveDownloads(),
getActiveDownloadDetails(),
getScrapeJobs(),
])
@@ -47,11 +49,24 @@ export default function Downloads() {
const startPolling = () => {
pollRef.current = setInterval(async () => {
const [activeData, scrapeData] = await Promise.all([
getActiveDownloads(),
getActiveDownloadDetails(),
getScrapeJobs(),
])
if (!activeData.error) {
const list = Array.isArray(activeData) ? activeData : []
// Calculate download speed (files/sec) based on completed delta
const newSpeeds = {}
for (const dl of list) {
const uid = dl.user_id
const prev = prevCompleted.current[uid] || 0
const delta = (dl.completed || 0) - prev
prevCompleted.current[uid] = dl.completed || 0
if (delta > 0) {
newSpeeds[uid] = (delta / 2).toFixed(1) // 2s poll interval
}
}
setSpeeds((prev) => ({ ...prev, ...newSpeeds }))
setActive((prev) => {
if (prev.length > 0 && list.length < prev.length) {
getDownloadHistory().then((h) => {
@@ -141,6 +156,11 @@ export default function Downloads() {
</p>
<p className="text-xs text-gray-500">
{dl.completed || 0} / {dl.total || '?'} files
{speeds[uid] && (
<span className="text-gray-400 ml-2">
({speeds[uid]} files/s)
</span>
)}
{dl.errors > 0 && (
<span className="text-red-400 ml-2">
({dl.errors} error{dl.errors !== 1 ? 's' : ''})
@@ -154,12 +174,26 @@ export default function Downloads() {
</div>
{/* Progress Bar */}
<div className="w-full bg-[#1a1a1a] rounded-full h-1.5">
<div className="w-full bg-[#1a1a1a] rounded-full h-1.5 mb-2">
<div
className="bg-[#0095f6] h-1.5 rounded-full transition-all duration-500"
style={{ width: `${progress}%` }}
/>
</div>
{/* Recent File Log */}
{dl.recentFiles && dl.recentFiles.length > 0 && (
<div className="space-y-0.5">
{dl.recentFiles.map((f, fi) => (
<div key={fi} className="flex items-center gap-2 text-xs">
<span className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${
f.status === 'ok' ? 'bg-green-400' : 'bg-red-400'
}`} />
<span className="text-gray-500 truncate">{f.filename?.slice(0, 50)}</span>
</div>
))}
</div>
)}
</div>
)
})}