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:
@@ -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 */}
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user