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):
|
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: int | None = None
|
||||||
last_sync_success: bool = True
|
last_sync_success: bool = True
|
||||||
|
|
||||||
|
|||||||
+13
-10
@@ -98,16 +98,19 @@ async def sync_library_periodically(
|
|||||||
if sync_freq == "manual":
|
if sync_freq == "manual":
|
||||||
await asyncio.sleep(3600)
|
await asyncio.sleep(3600)
|
||||||
continue
|
continue
|
||||||
elif sync_freq == "5min":
|
|
||||||
interval = 300
|
freq_to_seconds = {
|
||||||
elif sync_freq == "10min":
|
"5min": 300,
|
||||||
interval = 600
|
"10min": 600,
|
||||||
elif sync_freq == "30min":
|
"30min": 1800,
|
||||||
interval = 1800
|
"1hr": 3600,
|
||||||
elif sync_freq == "1hr":
|
"6hr": 21600,
|
||||||
interval = 3600
|
"12hr": 43200,
|
||||||
else:
|
"24hr": 86400,
|
||||||
interval = 600
|
"3d": 259200,
|
||||||
|
"7d": 604800,
|
||||||
|
}
|
||||||
|
interval = freq_to_seconds.get(sync_freq, 86400)
|
||||||
|
|
||||||
await asyncio.sleep(interval)
|
await asyncio.sleep(interval)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { syncStatus } from '$lib/stores/syncStatus.svelte';
|
import { syncStatus } from '$lib/stores/syncStatus.svelte';
|
||||||
import { playerStore } from '$lib/stores/player.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> = {
|
const phaseIcons: Record<string, typeof Users> = {
|
||||||
artists: Users,
|
artists: Users,
|
||||||
@@ -17,88 +28,133 @@
|
|||||||
let isComplete = $derived(
|
let isComplete = $derived(
|
||||||
!syncStatus.isActive && !syncStatus.error && syncStatus.showIndicator
|
!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>
|
</script>
|
||||||
|
|
||||||
{#if syncStatus.showIndicator}
|
{#if syncStatus.showIndicator}
|
||||||
<div
|
<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-24={playerStore.isPlayerVisible}
|
||||||
class:bottom-4={!playerStore.isPlayerVisible}
|
class:bottom-4={!playerStore.isPlayerVisible}
|
||||||
role="status"
|
role="status"
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
aria-label="Library sync progress"
|
aria-label="Library sync progress"
|
||||||
>
|
>
|
||||||
<div class="bg-base-200 rounded-box shadow-xl border border-base-300 p-4">
|
{#if syncStatus.isMinimized}
|
||||||
<div class="flex items-center justify-between gap-2 mb-3">
|
<!-- Minimized pill -->
|
||||||
<div class="flex items-center gap-2.5 min-w-0">
|
<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}
|
{#if isComplete}
|
||||||
<div class="bg-success/15 rounded-full p-1.5 shrink-0">
|
<Check class="h-3.5 w-3.5 text-success" />
|
||||||
<Check class="h-4 w-4 text-success" />
|
|
||||||
</div>
|
|
||||||
<span class="font-semibold text-sm text-success">Sync Complete</span>
|
|
||||||
{:else if syncStatus.error}
|
{:else if syncStatus.error}
|
||||||
<div class="bg-error/15 rounded-full p-1.5 shrink-0">
|
<TriangleAlert class="h-3.5 w-3.5 text-error" />
|
||||||
<TriangleAlert class="h-4 w-4 text-error" />
|
|
||||||
</div>
|
|
||||||
<span class="font-semibold text-sm text-error">Sync Failed</span>
|
|
||||||
{:else}
|
{:else}
|
||||||
<div class="bg-primary/10 rounded-full p-1.5 shrink-0">
|
<PhaseIcon class="h-3.5 w-3.5 text-primary {syncStatus.isActive ? 'animate-pulse' : ''}" />
|
||||||
<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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<span
|
||||||
class="btn btn-ghost btn-xs btn-circle shrink-0 opacity-50 hover:opacity-100"
|
class="text-xs font-medium truncate"
|
||||||
onclick={() => syncStatus.dismiss()}
|
class:text-success={isComplete}
|
||||||
aria-label="Dismiss sync indicator"
|
class:text-error={syncStatus.error}
|
||||||
>
|
>
|
||||||
<X class="h-3.5 w-3.5" />
|
{pillLabel}
|
||||||
</button>
|
</span>
|
||||||
</div>
|
<ChevronUp class="h-3.5 w-3.5 opacity-50 shrink-0" />
|
||||||
|
</button>
|
||||||
{#if syncStatus.isActive}
|
{:else}
|
||||||
<div
|
<!-- Expanded card -->
|
||||||
class="w-full bg-base-300 rounded-full h-1.5 mb-2 overflow-hidden"
|
<div class="bg-base-200 rounded-box shadow-xl border border-base-300 p-4 animate-slide-in-right">
|
||||||
role="progressbar"
|
<div class="flex items-center justify-between gap-2 mb-3">
|
||||||
aria-valuenow={syncStatus.progress}
|
<div class="flex items-center gap-2.5 min-w-0">
|
||||||
aria-valuemin={0}
|
{#if isComplete}
|
||||||
aria-valuemax={100}
|
<div class="bg-success/15 rounded-full p-1.5 shrink-0">
|
||||||
aria-label="{syncStatus.phaseLabel} progress"
|
<Check class="h-4 w-4 text-success" />
|
||||||
>
|
</div>
|
||||||
<div
|
<span class="font-semibold text-sm text-success">Sync Complete</span>
|
||||||
class="h-full rounded-full bg-primary transition-all duration-700 ease-out"
|
{:else if syncStatus.error}
|
||||||
style="width: {syncStatus.progress}%"
|
<div class="bg-error/15 rounded-full p-1.5 shrink-0">
|
||||||
></div>
|
<TriangleAlert class="h-4 w-4 text-error" />
|
||||||
</div>
|
</div>
|
||||||
|
<span class="font-semibold text-sm text-error">Sync Failed</span>
|
||||||
<div class="flex justify-between items-center text-xs text-base-content/50">
|
{:else}
|
||||||
{#if syncStatus.totalItems === 0}
|
<div class="bg-primary/10 rounded-full p-1.5 shrink-0">
|
||||||
<span>Cached ✓</span>
|
<PhaseIcon class="h-4 w-4 text-primary {syncStatus.isActive ? 'animate-pulse' : ''}" />
|
||||||
{:else}
|
</div>
|
||||||
<span>{syncStatus.processedItems} / {syncStatus.totalItems}</span>
|
<div class="min-w-0">
|
||||||
<span>{syncStatus.progress}%</span>
|
<span class="font-semibold text-sm">{syncStatus.phaseLabel}</span>
|
||||||
{/if}
|
{#if syncStatus.phaseNumber > 0}
|
||||||
</div>
|
<span class="text-xs text-base-content/40 ml-1">
|
||||||
|
{syncStatus.phaseNumber}/{syncStatus.totalPhases}
|
||||||
{#if syncStatus.currentItem}
|
</span>
|
||||||
<div class="text-xs text-base-content/35 truncate mt-1">
|
{/if}
|
||||||
{syncStatus.currentItem}
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
<div class="flex items-center gap-0.5 shrink-0">
|
||||||
{/if}
|
<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}
|
{#if syncStatus.isActive}
|
||||||
<p class="text-xs text-error/70 truncate mt-1">{syncStatus.error}</p>
|
<div
|
||||||
{/if}
|
class="w-full bg-base-300 rounded-full h-1.5 mb-2 overflow-hidden"
|
||||||
</div>
|
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>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import { onDestroy } from 'svelte';
|
import { onDestroy } from 'svelte';
|
||||||
|
|
||||||
interface LibrarySyncSettings {
|
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: number | null;
|
||||||
last_sync_success: boolean;
|
last_sync_success: boolean;
|
||||||
}
|
}
|
||||||
@@ -85,10 +85,19 @@
|
|||||||
<div class="label"><span class="label-text">Sync Frequency</span></div>
|
<div class="label"><span class="label-text">Sync Frequency</span></div>
|
||||||
<select bind:value={form.data.sync_frequency} class="select select-bordered">
|
<select bind:value={form.data.sync_frequency} class="select select-bordered">
|
||||||
<option value="manual">Manual only</option>
|
<option value="manual">Manual only</option>
|
||||||
<option value="5min">Every 5 minutes</option>
|
{#if form.data.sync_frequency === '5min'}
|
||||||
<option value="10min">Every 10 minutes</option>
|
<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="30min">Every 30 minutes</option>
|
||||||
<option value="1hr">Every hour</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>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ const AUTO_HIDE_ERROR_MS = 6000;
|
|||||||
function createSyncStatusStore() {
|
function createSyncStatusStore() {
|
||||||
let status = $state<SyncStatus>({ ...EMPTY_STATUS });
|
let status = $state<SyncStatus>({ ...EMPTY_STATUS });
|
||||||
let isDismissed = $state(false);
|
let isDismissed = $state(false);
|
||||||
|
let isMinimized = $state(false);
|
||||||
let showIndicator = $state(false);
|
let showIndicator = $state(false);
|
||||||
let connectionMode = $state<'sse' | 'polling'>('sse');
|
let connectionMode = $state<'sse' | 'polling'>('sse');
|
||||||
|
|
||||||
@@ -81,6 +82,7 @@ function createSyncStatusStore() {
|
|||||||
|
|
||||||
if (newStatus.is_syncing && !wasSyncing) {
|
if (newStatus.is_syncing && !wasSyncing) {
|
||||||
isDismissed = false;
|
isDismissed = false;
|
||||||
|
isMinimized = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wasSyncing && !newStatus.is_syncing && !newStatus.error_message && browser) {
|
if (wasSyncing && !newStatus.is_syncing && !newStatus.error_message && browser) {
|
||||||
@@ -254,6 +256,9 @@ function createSyncStatusStore() {
|
|||||||
get showIndicator() {
|
get showIndicator() {
|
||||||
return showIndicator && !isDismissed;
|
return showIndicator && !isDismissed;
|
||||||
},
|
},
|
||||||
|
get isMinimized() {
|
||||||
|
return isMinimized;
|
||||||
|
},
|
||||||
get connectionMode() {
|
get connectionMode() {
|
||||||
return connectionMode;
|
return connectionMode;
|
||||||
},
|
},
|
||||||
@@ -289,6 +294,14 @@ function createSyncStatusStore() {
|
|||||||
isDismissed = true;
|
isDismissed = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
minimize(): void {
|
||||||
|
isMinimized = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
expand(): void {
|
||||||
|
isMinimized = false;
|
||||||
|
},
|
||||||
|
|
||||||
checkStatus(): void {
|
checkStatus(): void {
|
||||||
void fetchStatus();
|
void fetchStatus();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
import { API } from '$lib/constants';
|
import { API } from '$lib/constants';
|
||||||
import { isAbortError } from '$lib/utils/errorHandling';
|
import { isAbortError } from '$lib/utils/errorHandling';
|
||||||
import type { Artist, Album } from '$lib/types';
|
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';
|
const CIRCUIT_BREAKER_CODE = 'CIRCUIT_BREAKER_OPEN';
|
||||||
|
|
||||||
@@ -71,6 +71,7 @@
|
|||||||
let syncing = false;
|
let syncing = false;
|
||||||
let error: string | null = null;
|
let error: string | null = null;
|
||||||
let errorCode: string | null = null;
|
let errorCode: string | null = null;
|
||||||
|
let syncFrequencyLabel: string | null = null;
|
||||||
|
|
||||||
let currentAlbumPage = 1;
|
let currentAlbumPage = 1;
|
||||||
let sortBy = 'date_added';
|
let sortBy = 'date_added';
|
||||||
@@ -88,13 +89,36 @@
|
|||||||
$: isConnectionError = errorCode === CIRCUIT_BREAKER_CODE ||
|
$: isConnectionError = errorCode === CIRCUIT_BREAKER_CODE ||
|
||||||
(error != null && /connection|DNS|not configured/i.test(error));
|
(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(() => {
|
onMount(() => {
|
||||||
recentlyAddedStore.initialize();
|
recentlyAddedStore.initialize();
|
||||||
loadArtists();
|
loadArtists();
|
||||||
fetchAlbums();
|
fetchAlbums();
|
||||||
loadStats();
|
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() {
|
async function loadArtists() {
|
||||||
try {
|
try {
|
||||||
const url = API.library.artists(ARTIST_CAROUSEL_LIMIT, 0, 'name', 'asc');
|
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}
|
{stats.artist_count} artists • {stats.album_count} albums • Last sync: {lastSyncText}
|
||||||
{/if}
|
{/if}
|
||||||
</p>
|
</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>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-primary gap-1"
|
class="btn btn-sm btn-primary gap-1"
|
||||||
|
|||||||
Reference in New Issue
Block a user