fix: increase sync time default/max + minimizable sync notif (#12)
This commit is contained in:
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user