Add scheduled sync, disk space awareness, and subtitle downloads

- auto-sync daemon: background thread checks every hour and syncs followed
  channels for users with sync_interval_hours set (6/12/24h options)
- disk stats: /api/stats now returns total/used/free/download bytes;
  Stats page shows a disk usage bar
- subtitles: subtitle_langs setting (e.g. "en,sv") passed through all
  download paths; yt-dlp writes .srt files alongside the video
- Settings page: sync interval dropdown + subtitle languages input

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-26 20:36:50 +02:00
parent 3abbd5749e
commit ea99b74ba8
9 changed files with 150 additions and 6 deletions

View File

@@ -11,6 +11,14 @@ function fmt(seconds) {
return `${h}h ${m}m`;
}
function fmtBytes(bytes) {
if (!bytes) return "0 B";
const units = ["B", "KB", "MB", "GB", "TB"];
let i = 0, v = bytes;
while (v >= 1024 && i < units.length - 1) { v /= 1024; i++; }
return `${v.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
}
function StatCard({ label, value, sub }) {
return (
<div className="bg-zinc-900 rounded-2xl p-4 flex flex-col gap-1">
@@ -170,6 +178,29 @@ export default function Stats() {
)}
</div>
{/* Disk usage */}
{data.disk?.total_bytes && (
<div className="bg-zinc-900 rounded-2xl p-5 flex flex-col gap-3">
<h2 className="text-xs font-semibold text-zinc-500 uppercase tracking-wider">Disk usage</h2>
<div className="flex flex-col gap-2">
<div className="flex justify-between text-sm">
<span className="text-zinc-400">Downloads</span>
<span className="text-zinc-300 font-mono">{fmtBytes(data.disk.download_bytes)}</span>
</div>
<div className="h-2 bg-zinc-800 rounded-full overflow-hidden">
<div
className="h-full bg-accent/70 rounded-full transition-all"
style={{ width: `${Math.min((data.disk.used_bytes / data.disk.total_bytes) * 100, 100)}%` }}
/>
</div>
<div className="flex justify-between text-[11px] text-zinc-600">
<span>{fmtBytes(data.disk.used_bytes)} used</span>
<span>{fmtBytes(data.disk.free_bytes)} free · {fmtBytes(data.disk.total_bytes)} total</span>
</div>
</div>
</div>
)}
{/* Taste profile */}
{topTags.length > 0 && (
<div className="bg-zinc-900 rounded-2xl p-5 flex flex-col gap-3">