fix: increase sync time default/max + minimizable sync notif (#12)

This commit is contained in:
Harvey
2026-04-05 02:52:28 +01:00
committed by GitHub
parent 687a925545
commit 48c0a94a47
6 changed files with 198 additions and 80 deletions
+1 -1
View File
@@ -160,7 +160,7 @@ class LocalFilesVerifyResponse(AppStruct):
class LidarrSettings(AppStruct):
sync_frequency: Literal["manual", "5min", "10min", "30min", "1hr"] = "10min"
sync_frequency: Literal["manual", "5min", "10min", "30min", "1hr", "6hr", "12hr", "24hr", "3d", "7d"] = "24hr"
last_sync: int | None = None
last_sync_success: bool = True
+13 -10
View File
@@ -98,16 +98,19 @@ async def sync_library_periodically(
if sync_freq == "manual":
await asyncio.sleep(3600)
continue
elif sync_freq == "5min":
interval = 300
elif sync_freq == "10min":
interval = 600
elif sync_freq == "30min":
interval = 1800
elif sync_freq == "1hr":
interval = 3600
else:
interval = 600
freq_to_seconds = {
"5min": 300,
"10min": 600,
"30min": 1800,
"1hr": 3600,
"6hr": 21600,
"12hr": 43200,
"24hr": 86400,
"3d": 259200,
"7d": 604800,
}
interval = freq_to_seconds.get(sync_freq, 86400)
await asyncio.sleep(interval)
@@ -1,7 +1,18 @@
<script lang="ts">
import { syncStatus } from '$lib/stores/syncStatus.svelte';
import { playerStore } from '$lib/stores/player.svelte';
import { Users, Disc3, Image, Loader2, Check, TriangleAlert, X, Search } from 'lucide-svelte';
import {
Users,
Disc3,
Image,
Loader2,
Check,
TriangleAlert,
X,
Search,
Minus,
ChevronUp
} from 'lucide-svelte';
const phaseIcons: Record<string, typeof Users> = {
artists: Users,
@@ -17,88 +28,133 @@
let isComplete = $derived(
!syncStatus.isActive && !syncStatus.error && syncStatus.showIndicator
);
let pillLabel = $derived.by(() => {
if (isComplete) return 'Sync Complete';
if (syncStatus.error) return 'Sync Failed';
return `${syncStatus.phaseLabel}${syncStatus.progress}%`;
});
</script>
{#if syncStatus.showIndicator}
<div
class="fixed right-4 z-40 animate-slide-in-right w-80 max-w-[calc(100vw-2rem)]"
class="fixed right-4 z-40 w-80 max-w-[calc(100vw-2rem)]"
class:bottom-24={playerStore.isPlayerVisible}
class:bottom-4={!playerStore.isPlayerVisible}
role="status"
aria-live="polite"
aria-label="Library sync progress"
>
<div class="bg-base-200 rounded-box shadow-xl border border-base-300 p-4">
<div class="flex items-center justify-between gap-2 mb-3">
<div class="flex items-center gap-2.5 min-w-0">
{#if syncStatus.isMinimized}
<!-- Minimized pill -->
<button
class="flex items-center gap-2 bg-base-200 rounded-full shadow-xl border border-base-300 pl-3 pr-2 py-1.5 w-auto max-w-full cursor-pointer hover:bg-base-300/80 transition-colors animate-slide-in-right ml-auto"
onclick={() => syncStatus.expand()}
aria-label="Expand sync progress"
>
<div class="shrink-0">
{#if isComplete}
<div class="bg-success/15 rounded-full p-1.5 shrink-0">
<Check class="h-4 w-4 text-success" />
</div>
<span class="font-semibold text-sm text-success">Sync Complete</span>
<Check class="h-3.5 w-3.5 text-success" />
{:else if syncStatus.error}
<div class="bg-error/15 rounded-full p-1.5 shrink-0">
<TriangleAlert class="h-4 w-4 text-error" />
</div>
<span class="font-semibold text-sm text-error">Sync Failed</span>
<TriangleAlert class="h-3.5 w-3.5 text-error" />
{:else}
<div class="bg-primary/10 rounded-full p-1.5 shrink-0">
<PhaseIcon class="h-4 w-4 text-primary {syncStatus.isActive ? 'animate-pulse' : ''}" />
</div>
<div class="min-w-0">
<span class="font-semibold text-sm">{syncStatus.phaseLabel}</span>
{#if syncStatus.phaseNumber > 0}
<span class="text-xs text-base-content/40 ml-1">
{syncStatus.phaseNumber}/{syncStatus.totalPhases}
</span>
{/if}
</div>
<PhaseIcon class="h-3.5 w-3.5 text-primary {syncStatus.isActive ? 'animate-pulse' : ''}" />
{/if}
</div>
<button
class="btn btn-ghost btn-xs btn-circle shrink-0 opacity-50 hover:opacity-100"
onclick={() => syncStatus.dismiss()}
aria-label="Dismiss sync indicator"
<span
class="text-xs font-medium truncate"
class:text-success={isComplete}
class:text-error={syncStatus.error}
>
<X class="h-3.5 w-3.5" />
</button>
</div>
{#if syncStatus.isActive}
<div
class="w-full bg-base-300 rounded-full h-1.5 mb-2 overflow-hidden"
role="progressbar"
aria-valuenow={syncStatus.progress}
aria-valuemin={0}
aria-valuemax={100}
aria-label="{syncStatus.phaseLabel} progress"
>
<div
class="h-full rounded-full bg-primary transition-all duration-700 ease-out"
style="width: {syncStatus.progress}%"
></div>
</div>
<div class="flex justify-between items-center text-xs text-base-content/50">
{#if syncStatus.totalItems === 0}
<span>Cached ✓</span>
{:else}
<span>{syncStatus.processedItems} / {syncStatus.totalItems}</span>
<span>{syncStatus.progress}%</span>
{/if}
</div>
{#if syncStatus.currentItem}
<div class="text-xs text-base-content/35 truncate mt-1">
{syncStatus.currentItem}
{pillLabel}
</span>
<ChevronUp class="h-3.5 w-3.5 opacity-50 shrink-0" />
</button>
{:else}
<!-- Expanded card -->
<div class="bg-base-200 rounded-box shadow-xl border border-base-300 p-4 animate-slide-in-right">
<div class="flex items-center justify-between gap-2 mb-3">
<div class="flex items-center gap-2.5 min-w-0">
{#if isComplete}
<div class="bg-success/15 rounded-full p-1.5 shrink-0">
<Check class="h-4 w-4 text-success" />
</div>
<span class="font-semibold text-sm text-success">Sync Complete</span>
{:else if syncStatus.error}
<div class="bg-error/15 rounded-full p-1.5 shrink-0">
<TriangleAlert class="h-4 w-4 text-error" />
</div>
<span class="font-semibold text-sm text-error">Sync Failed</span>
{:else}
<div class="bg-primary/10 rounded-full p-1.5 shrink-0">
<PhaseIcon class="h-4 w-4 text-primary {syncStatus.isActive ? 'animate-pulse' : ''}" />
</div>
<div class="min-w-0">
<span class="font-semibold text-sm">{syncStatus.phaseLabel}</span>
{#if syncStatus.phaseNumber > 0}
<span class="text-xs text-base-content/40 ml-1">
{syncStatus.phaseNumber}/{syncStatus.totalPhases}
</span>
{/if}
</div>
{/if}
</div>
{/if}
{/if}
<div class="flex items-center gap-0.5 shrink-0">
<button
class="btn btn-ghost btn-xs btn-circle opacity-50 hover:opacity-100"
onclick={() => syncStatus.minimize()}
aria-label="Minimize sync progress"
title="Minimize"
>
<Minus class="h-3.5 w-3.5" />
</button>
<button
class="btn btn-ghost btn-xs btn-circle opacity-50 hover:opacity-100"
onclick={() => syncStatus.dismiss()}
aria-label="Dismiss notification"
title="Dismiss"
>
<X class="h-3.5 w-3.5" />
</button>
</div>
</div>
{#if syncStatus.error}
<p class="text-xs text-error/70 truncate mt-1">{syncStatus.error}</p>
{/if}
</div>
{#if syncStatus.isActive}
<div
class="w-full bg-base-300 rounded-full h-1.5 mb-2 overflow-hidden"
role="progressbar"
aria-valuenow={syncStatus.progress}
aria-valuemin={0}
aria-valuemax={100}
aria-label="{syncStatus.phaseLabel} progress"
>
<div
class="h-full rounded-full bg-primary transition-all duration-700 ease-out"
style="width: {syncStatus.progress}%"
></div>
</div>
<div class="flex justify-between items-center text-xs text-base-content/50">
{#if syncStatus.totalItems === 0}
<span>Cached ✓</span>
{:else}
<span>{syncStatus.processedItems} / {syncStatus.totalItems}</span>
<span>{syncStatus.progress}%</span>
{/if}
</div>
{#if syncStatus.currentItem}
<div class="text-xs text-base-content/35 truncate mt-1">
{syncStatus.currentItem}
</div>
{/if}
{/if}
{#if syncStatus.error}
<p class="text-xs text-error/70 truncate mt-1">{syncStatus.error}</p>
{/if}
</div>
{/if}
</div>
{/if}
@@ -6,7 +6,7 @@
import { onDestroy } from 'svelte';
interface LibrarySyncSettings {
sync_frequency: 'manual' | '5min' | '10min' | '30min' | '1hr';
sync_frequency: 'manual' | '5min' | '10min' | '30min' | '1hr' | '6hr' | '12hr' | '24hr' | '3d' | '7d';
last_sync: number | null;
last_sync_success: boolean;
}
@@ -85,10 +85,19 @@
<div class="label"><span class="label-text">Sync Frequency</span></div>
<select bind:value={form.data.sync_frequency} class="select select-bordered">
<option value="manual">Manual only</option>
<option value="5min">Every 5 minutes</option>
<option value="10min">Every 10 minutes</option>
{#if form.data.sync_frequency === '5min'}
<option value="5min">Every 5 minutes (legacy)</option>
{/if}
{#if form.data.sync_frequency === '10min'}
<option value="10min">Every 10 minutes (legacy)</option>
{/if}
<option value="30min">Every 30 minutes</option>
<option value="1hr">Every hour</option>
<option value="6hr">Every 6 hours</option>
<option value="12hr">Every 12 hours</option>
<option value="24hr">Every 24 hours</option>
<option value="3d">Every 3 days</option>
<option value="7d">Every 7 days</option>
</select>
</label>
@@ -48,6 +48,7 @@ const AUTO_HIDE_ERROR_MS = 6000;
function createSyncStatusStore() {
let status = $state<SyncStatus>({ ...EMPTY_STATUS });
let isDismissed = $state(false);
let isMinimized = $state(false);
let showIndicator = $state(false);
let connectionMode = $state<'sse' | 'polling'>('sse');
@@ -81,6 +82,7 @@ function createSyncStatusStore() {
if (newStatus.is_syncing && !wasSyncing) {
isDismissed = false;
isMinimized = false;
}
if (wasSyncing && !newStatus.is_syncing && !newStatus.error_message && browser) {
@@ -254,6 +256,9 @@ function createSyncStatusStore() {
get showIndicator() {
return showIndicator && !isDismissed;
},
get isMinimized() {
return isMinimized;
},
get connectionMode() {
return connectionMode;
},
@@ -289,6 +294,14 @@ function createSyncStatusStore() {
isDismissed = true;
},
minimize(): void {
isMinimized = true;
},
expand(): void {
isMinimized = false;
},
checkStatus(): void {
void fetchStatus();
}
+38 -1
View File
@@ -13,7 +13,7 @@
import { API } from '$lib/constants';
import { isAbortError } from '$lib/utils/errorHandling';
import type { Artist, Album } from '$lib/types';
import { CircleX, X, RefreshCw, ChevronRight, Search, Loader2 } from 'lucide-svelte';
import { CircleX, X, RefreshCw, ChevronRight, Search, Loader2, Settings2 } from 'lucide-svelte';
const CIRCUIT_BREAKER_CODE = 'CIRCUIT_BREAKER_OPEN';
@@ -71,6 +71,7 @@
let syncing = false;
let error: string | null = null;
let errorCode: string | null = null;
let syncFrequencyLabel: string | null = null;
let currentAlbumPage = 1;
let sortBy = 'date_added';
@@ -88,13 +89,36 @@
$: isConnectionError = errorCode === CIRCUIT_BREAKER_CODE ||
(error != null && /connection|DNS|not configured/i.test(error));
const FREQ_LABELS: Record<string, string> = {
manual: 'Manual sync only',
'5min': 'Auto-syncs every 5 minutes',
'10min': 'Auto-syncs every 10 minutes',
'30min': 'Auto-syncs every 30 minutes',
'1hr': 'Auto-syncs every hour',
'6hr': 'Auto-syncs every 6 hours',
'12hr': 'Auto-syncs every 12 hours',
'24hr': 'Auto-syncs every 24 hours',
'3d': 'Auto-syncs every 3 days',
'7d': 'Auto-syncs every 7 days'
};
onMount(() => {
recentlyAddedStore.initialize();
loadArtists();
fetchAlbums();
loadStats();
loadSyncFrequency();
});
async function loadSyncFrequency() {
try {
const data = await api.global.get<{ sync_frequency: string }>('/api/v1/settings/lidarr');
syncFrequencyLabel = FREQ_LABELS[data.sync_frequency] ?? null;
} catch {
// Silently omit frequency hint if settings can't be loaded
}
}
async function loadArtists() {
try {
const url = API.library.artists(ARTIST_CAROUSEL_LIMIT, 0, 'name', 'asc');
@@ -266,6 +290,19 @@
{stats.artist_count} artists • {stats.album_count} albums • Last sync: {lastSyncText}
{/if}
</p>
{#if syncFrequencyLabel}
<p class="text-base-content/50 text-xs mt-0.5 flex items-center gap-1">
{syncFrequencyLabel}
<a
href="/settings?tab=lidarr"
class="inline-flex hover:text-base-content/70 transition-colors"
aria-label="Configure sync frequency"
title="Configure sync frequency"
>
<Settings2 class="h-3 w-3" />
</a>
</p>
{/if}
</div>
<button
class="btn btn-sm btn-primary gap-1"