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

@@ -650,6 +650,25 @@ export default function SettingsPage() {
<DiagnosticSection />
<SubscriptionImportSection />
{/* Sync */}
<Section title="Sync">
<Row
label="Auto-sync interval"
hint="How often to automatically sync your followed channels in the background."
>
<select
value={s?.sync_interval_hours ?? 0}
onChange={(e) => set({ sync_interval_hours: Number(e.target.value) })}
className="bg-zinc-800 text-zinc-200 text-sm rounded-lg px-3 py-1.5 border border-zinc-700 focus:outline-none focus:border-accent"
>
<option value={0}>Off</option>
<option value={6}>Every 6 hours</option>
<option value={12}>Every 12 hours</option>
<option value={24}>Every 24 hours</option>
</select>
</Row>
</Section>
{/* Download quality */}
<Section title="Download quality">
<Row
@@ -691,6 +710,18 @@ export default function SettingsPage() {
onChange={(v) => set({ auto_download_on_sync: v })}
/>
</Row>
<Row
label="Subtitle languages"
hint={'Download subtitles for these languages. e.g. "en" or "en,sv". Leave blank to skip.'}
>
<input
type="text"
value={s?.subtitle_langs ?? ""}
onChange={(e) => set({ subtitle_langs: e.target.value })}
placeholder="en, sv, …"
className="bg-zinc-800 text-zinc-200 text-sm rounded-lg px-3 py-1.5 border border-zinc-700 focus:outline-none focus:border-accent w-36"
/>
</Row>
</Section>
{/* Feed */}

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">