mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
fix(connection): ignore spurious navigator.onLine offline hint in Tauri (#1234)
This commit is contained in:
@@ -57,6 +57,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
* Gapless auto-advance no longer leaves the playbar on the previous track; missed `audio:track_switched` is reconciled from engine position with seek guards so backward seek is not treated as a gapless switch.
|
* Gapless auto-advance no longer leaves the playbar on the previous track; missed `audio:track_switched` is reconciled from engine position with seek guards so backward seek is not treated as a gapless switch.
|
||||||
* Local library index stores `replayGainPeak` (migration 015) so anti-clipping peak is available on index-first paths without an extra network round-trip.
|
* Local library index stores `replayGainPeak` (migration 015) so anti-clipping peak is available on index-first paths without an extra network round-trip.
|
||||||
|
|
||||||
|
### Connection — ignore spurious offline hint on desktop
|
||||||
|
|
||||||
|
**By [@cucadmuh](https://github.com/cucadmuh), reported by mikmik on the Psysonic Discord, PR [#1234](https://github.com/Psychotoxical/psysonic/pull/1234)**
|
||||||
|
|
||||||
|
* Desktop builds no longer get stuck showing "offline" when WebKitGTK leaves `navigator.onLine` stuck at `false` while the server is actually reachable — the app now confirms with a real server probe instead of trusting that hint, so browse and playback keep working. Web builds are unchanged.
|
||||||
|
* Pending favorite/rating sync now flushes when the server actually becomes reachable again, rather than relying on a browser `online` event that may never fire on desktop.
|
||||||
|
|
||||||
|
|
||||||
## [1.49.0] - 2026-06-29
|
## [1.49.0] - 2026-06-29
|
||||||
|
|
||||||
|
|||||||
@@ -180,6 +180,7 @@ const CONTRIBUTOR_ENTRIES = [
|
|||||||
'Timeline play history — session buffer + play_session bootstrap across queue replace; pin current to top; history replay inserts in-place (PR #1204)',
|
'Timeline play history — session buffer + play_session bootstrap across queue replace; pin current to top; history replay inserts in-place (PR #1204)',
|
||||||
'Playback — ReplayGain index prefetch, gapless playbar sync, library replayGainPeak column, live RG refresh after sync (PR #1231)',
|
'Playback — ReplayGain index prefetch, gapless playbar sync, library replayGainPeak column, live RG refresh after sync (PR #1231)',
|
||||||
'Artists browse — album vs track credit mode toggle, starred favorites in both modes, persisted credit mode, SQL letter-bucket filter (PR #1232)',
|
'Artists browse — album vs track credit mode toggle, starred favorites in both modes, persisted credit mode, SQL letter-bucket filter (PR #1232)',
|
||||||
|
'Connection — ignore spurious WebKitGTK navigator.onLine offline hint on desktop; confirm via server probe; flush pending favorite/rating sync on real reachability (report: mikmik on Psysonic Discord, PR #1234)',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useAuthStore } from '../store/authStore';
|
import { useAuthStore } from '../store/authStore';
|
||||||
|
import { isNavigatorOfflineHint } from '@/lib/network/navigatorOnlineHint';
|
||||||
import type { CoverServerScope } from './types';
|
import type { CoverServerScope } from './types';
|
||||||
|
|
||||||
/** Per-server reachability — active/playback use navigator + configured server */
|
/** Per-server reachability — active/playback use navigator + configured server */
|
||||||
@@ -6,7 +7,7 @@ export function coverServerReachable(scope: CoverServerScope): boolean {
|
|||||||
if (scope.kind === 'server') {
|
if (scope.kind === 'server') {
|
||||||
return !!scope.url && !!scope.username;
|
return !!scope.url && !!scope.username;
|
||||||
}
|
}
|
||||||
if (typeof navigator !== 'undefined' && !navigator.onLine) return false;
|
if (isNavigatorOfflineHint()) return false;
|
||||||
const active = useAuthStore.getState().getActiveServer();
|
const active = useAuthStore.getState().getActiveServer();
|
||||||
const baseUrl = useAuthStore.getState().getBaseUrl();
|
const baseUrl = useAuthStore.getState().getBaseUrl();
|
||||||
return !!(active && baseUrl);
|
return !!(active && baseUrl);
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import {
|
|||||||
} from '@/store/devOfflineBrowseStore';
|
} from '@/store/devOfflineBrowseStore';
|
||||||
import { useConnectionStatus } from '@/lib/hooks/useConnectionStatus';
|
import { useConnectionStatus } from '@/lib/hooks/useConnectionStatus';
|
||||||
import { isActiveServerReachable } from '@/lib/network/activeServerReachability';
|
import { isActiveServerReachable } from '@/lib/network/activeServerReachability';
|
||||||
|
import { isNavigatorOfflineHint } from '@/lib/network/navigatorOnlineHint';
|
||||||
|
|
||||||
/** True when browse/detail pages should use local-bytes-only data sources. */
|
/** True when browse/detail pages should use local-bytes-only data sources. */
|
||||||
export function isOfflineBrowseActive(): boolean {
|
export function isOfflineBrowseActive(): boolean {
|
||||||
if (isDevOfflineBrowseForced()) return true;
|
if (isDevOfflineBrowseForced()) return true;
|
||||||
if (typeof navigator !== 'undefined' && !navigator.onLine) return true;
|
if (isNavigatorOfflineHint()) return true;
|
||||||
return !isActiveServerReachable();
|
return !isActiveServerReachable();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,6 +23,6 @@ export function useOfflineBrowseActive(): boolean {
|
|||||||
useConnectionStatus();
|
useConnectionStatus();
|
||||||
|
|
||||||
if (import.meta.env.DEV && devForceOffline) return true;
|
if (import.meta.env.DEV && devForceOffline) return true;
|
||||||
if (typeof navigator !== 'undefined' && !navigator.onLine) return true;
|
if (isNavigatorOfflineHint()) return true;
|
||||||
return !isActiveServerReachable();
|
return !isActiveServerReachable();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ vi.mock('@/lib/api/subsonicStarRating', () => ({
|
|||||||
|
|
||||||
import { usePlayerStore } from '@/features/playback/store/playerStore';
|
import { usePlayerStore } from '@/features/playback/store/playerStore';
|
||||||
import type { Track } from '@/lib/media/trackTypes';
|
import type { Track } from '@/lib/media/trackTypes';
|
||||||
|
import {
|
||||||
|
resetActiveServerConnectionSnapshot,
|
||||||
|
setActiveServerReachable,
|
||||||
|
} from '@/lib/network/activeServerReachability';
|
||||||
import { queueSongStar, queueSongRating, _resetPendingStarSyncForTest } from '@/features/playback/store/pendingStarSync';
|
import { queueSongStar, queueSongRating, _resetPendingStarSyncForTest } from '@/features/playback/store/pendingStarSync';
|
||||||
import {
|
import {
|
||||||
getCachedTrack,
|
getCachedTrack,
|
||||||
@@ -26,6 +30,8 @@ const track = (id: string): Track => ({
|
|||||||
describe('pendingStarSync', () => {
|
describe('pendingStarSync', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
|
resetActiveServerConnectionSnapshot();
|
||||||
|
setActiveServerReachable(true);
|
||||||
starMock.mockReset().mockResolvedValue(undefined);
|
starMock.mockReset().mockResolvedValue(undefined);
|
||||||
unstarMock.mockReset().mockResolvedValue(undefined);
|
unstarMock.mockReset().mockResolvedValue(undefined);
|
||||||
setRatingMock.mockReset().mockResolvedValue(undefined);
|
setRatingMock.mockReset().mockResolvedValue(undefined);
|
||||||
@@ -75,6 +81,19 @@ describe('pendingStarSync', () => {
|
|||||||
expect(usePlayerStore.getState().starredOverrides.t1).toBe(true); // override survives (no rollback)
|
expect(usePlayerStore.getState().starredOverrides.t1).toBe(true); // override survives (no rollback)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('flushes pending stars when the active server becomes reachable', async () => {
|
||||||
|
starMock.mockRejectedValue(new Error('offline'));
|
||||||
|
queueSongStar('t1', true);
|
||||||
|
await vi.advanceTimersByTimeAsync(0);
|
||||||
|
expect(starMock).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
setActiveServerReachable(false);
|
||||||
|
setActiveServerReachable(true);
|
||||||
|
await vi.advanceTimersByTimeAsync(0);
|
||||||
|
|
||||||
|
expect(starMock.mock.calls.length).toBeGreaterThan(1);
|
||||||
|
});
|
||||||
|
|
||||||
it('passes serverId through to star/unstar for cross-server favorites', async () => {
|
it('passes serverId through to star/unstar for cross-server favorites', async () => {
|
||||||
queueSongStar('t1', true, 'srv-b');
|
queueSongStar('t1', true, 'srv-b');
|
||||||
await vi.runAllTimersAsync();
|
await vi.runAllTimersAsync();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { setRating, star, unstar } from '@/lib/api/subsonicStarRating';
|
import { setRating, star, unstar } from '@/lib/api/subsonicStarRating';
|
||||||
import { usePlayerStore } from '@/features/playback/store/playerStore';
|
import { usePlayerStore } from '@/features/playback/store/playerStore';
|
||||||
import { patchCachedTrack } from '@/features/playback/store/queueTrackResolver';
|
import { patchCachedTrack } from '@/features/playback/store/queueTrackResolver';
|
||||||
|
import { onActiveServerBecameReachable } from '@/lib/network/activeServerReachability';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* F4 — pending-sync for **song** star + rating (spec §6.5 / R7-18).
|
* F4 — pending-sync for **song** star + rating (spec §6.5 / R7-18).
|
||||||
@@ -11,7 +12,8 @@ import { patchCachedTrack } from '@/features/playback/store/queueTrackResolver';
|
|||||||
*
|
*
|
||||||
* 1. Set the override optimistically (instant UI).
|
* 1. Set the override optimistically (instant UI).
|
||||||
* 2. Retry the Subsonic API (`star` / `unstar` / `setRating`) with exponential
|
* 2. Retry the Subsonic API (`star` / `unstar` / `setRating`) with exponential
|
||||||
* backoff; flush immediately on `online` / window focus.
|
* backoff; flush immediately when the active server becomes reachable again
|
||||||
|
* (`onActiveServerBecameReachable`) or on window focus.
|
||||||
* 3. On **star** success: KEEP the override — list views read it — and patch
|
* 3. On **star** success: KEEP the override — list views read it — and patch
|
||||||
* the in-memory `Track`. F3 index patch-on-use runs in the API layer.
|
* the in-memory `Track`. F3 index patch-on-use runs in the API layer.
|
||||||
* (Ratings clear on success; see `onRatingSuccess`.)
|
* (Ratings clear on success; see `onRatingSuccess`.)
|
||||||
@@ -42,8 +44,8 @@ function armListeners(): void {
|
|||||||
const flushAll = () => {
|
const flushAll = () => {
|
||||||
for (const k of pending.keys()) schedule(k, 0);
|
for (const k of pending.keys()) schedule(k, 0);
|
||||||
};
|
};
|
||||||
window.addEventListener('online', flushAll);
|
|
||||||
window.addEventListener('focus', flushAll);
|
window.addEventListener('focus', flushAll);
|
||||||
|
onActiveServerBecameReachable(flushAll);
|
||||||
}
|
}
|
||||||
|
|
||||||
function schedule(k: string, delayMs: number): void {
|
function schedule(k: string, delayMs: number): void {
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ vi.mock('@/lib/perf/perfFlags', () => ({
|
|||||||
usePerfProbeFlags: () => ({ disableBackgroundPolling: false }),
|
usePerfProbeFlags: () => ({ disableBackgroundPolling: false }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const isTauri = vi.fn(() => false);
|
||||||
|
vi.mock('@tauri-apps/api/core', () => ({
|
||||||
|
isTauri: () => isTauri(),
|
||||||
|
}));
|
||||||
|
|
||||||
import { pingWithCredentialsForProfile } from '@/lib/api/subsonic';
|
import { pingWithCredentialsForProfile } from '@/lib/api/subsonic';
|
||||||
import { useDevOfflineBrowseStore } from '@/features/offline';
|
import { useDevOfflineBrowseStore } from '@/features/offline';
|
||||||
import { resetActiveServerConnectionSnapshot, setConnectionStatus } from '@/lib/network/activeServerReachability';
|
import { resetActiveServerConnectionSnapshot, setConnectionStatus } from '@/lib/network/activeServerReachability';
|
||||||
@@ -27,6 +32,7 @@ beforeEach(() => {
|
|||||||
resetActiveServerConnectionSnapshot();
|
resetActiveServerConnectionSnapshot();
|
||||||
invalidateReachableEndpointCache();
|
invalidateReachableEndpointCache();
|
||||||
useDevOfflineBrowseStore.getState().setForceOffline(false);
|
useDevOfflineBrowseStore.getState().setForceOffline(false);
|
||||||
|
isTauri.mockReturnValue(false);
|
||||||
vi.mocked(pingWithCredentialsForProfile).mockReset();
|
vi.mocked(pingWithCredentialsForProfile).mockReset();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -190,6 +196,51 @@ describe('useConnectionStatus DEV offline toggle', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('useConnectionStatus navigator.onLine (Tauri)', () => {
|
||||||
|
it('probes the server when navigator.onLine is false inside Tauri', async () => {
|
||||||
|
isTauri.mockReturnValue(true);
|
||||||
|
Object.defineProperty(navigator, 'onLine', { configurable: true, value: false });
|
||||||
|
|
||||||
|
seedDualAddressServer();
|
||||||
|
vi.mocked(pingWithCredentialsForProfile).mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
type: 'navidrome',
|
||||||
|
serverVersion: '0.62.0',
|
||||||
|
openSubsonic: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useConnectionStatus());
|
||||||
|
await waitFor(() => expect(result.current.status).toBe('connected'));
|
||||||
|
expect(vi.mocked(pingWithCredentialsForProfile).mock.calls.length).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('re-probes on offline event in Tauri instead of disconnecting', async () => {
|
||||||
|
isTauri.mockReturnValue(true);
|
||||||
|
Object.defineProperty(navigator, 'onLine', { configurable: true, value: false });
|
||||||
|
|
||||||
|
seedDualAddressServer();
|
||||||
|
vi.mocked(pingWithCredentialsForProfile).mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
type: 'navidrome',
|
||||||
|
serverVersion: '0.62.0',
|
||||||
|
openSubsonic: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useConnectionStatus());
|
||||||
|
await waitFor(() => expect(result.current.status).toBe('connected'));
|
||||||
|
const callsAfterMount = vi.mocked(pingWithCredentialsForProfile).mock.calls.length;
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
window.dispatchEvent(new Event('offline'));
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(vi.mocked(pingWithCredentialsForProfile).mock.calls.length).toBeGreaterThan(callsAfterMount),
|
||||||
|
);
|
||||||
|
expect(result.current.status).toBe('connected');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('useConnectionStatus shared status', () => {
|
describe('useConnectionStatus shared status', () => {
|
||||||
it('keeps all hook instances in sync when connection status changes', () => {
|
it('keeps all hook instances in sync when connection status changes', () => {
|
||||||
const shell = renderHook(() => useConnectionStatus());
|
const shell = renderHook(() => useConnectionStatus());
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
subscribeConnectionStatus,
|
subscribeConnectionStatus,
|
||||||
type ConnectionStatus,
|
type ConnectionStatus,
|
||||||
} from '@/lib/network/activeServerReachability';
|
} from '@/lib/network/activeServerReachability';
|
||||||
|
import { isNavigatorOfflineHint } from '@/lib/network/navigatorOnlineHint';
|
||||||
import { usePerfProbeFlags } from '@/lib/perf/perfFlags';
|
import { usePerfProbeFlags } from '@/lib/perf/perfFlags';
|
||||||
import {
|
import {
|
||||||
isDevOfflineBrowseForced,
|
isDevOfflineBrowseForced,
|
||||||
@@ -52,7 +53,7 @@ export function useConnectionStatus() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!navigator.onLine) {
|
if (isNavigatorOfflineHint()) {
|
||||||
setActiveServerReachable(false);
|
setActiveServerReachable(false);
|
||||||
setConnectionStatus('disconnected');
|
setConnectionStatus('disconnected');
|
||||||
return;
|
return;
|
||||||
@@ -150,8 +151,14 @@ export function useConnectionStatus() {
|
|||||||
check();
|
check();
|
||||||
};
|
};
|
||||||
const handleOffline = () => {
|
const handleOffline = () => {
|
||||||
setActiveServerReachable(false);
|
// WebKitGTK can spuriously fire `offline` while HTTP still works — re-probe
|
||||||
setConnectionStatus('disconnected');
|
// on desktop instead of hard-disconnecting (see navigatorOnlineHint).
|
||||||
|
if (isNavigatorOfflineHint()) {
|
||||||
|
setActiveServerReachable(false);
|
||||||
|
setConnectionStatus('disconnected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void check();
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('online', handleOnline);
|
window.addEventListener('online', handleOnline);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { isDevOfflineBrowseForced } from '@/store/devOfflineBrowseStore';
|
import { isDevOfflineBrowseForced } from '@/store/devOfflineBrowseStore';
|
||||||
|
import { isNavigatorOfflineHint } from '@/lib/network/navigatorOnlineHint';
|
||||||
|
|
||||||
export type ConnectionStatus = 'connected' | 'disconnected' | 'checking';
|
export type ConnectionStatus = 'connected' | 'disconnected' | 'checking';
|
||||||
|
|
||||||
@@ -53,9 +54,9 @@ export function resetActiveServerConnectionSnapshot(): void {
|
|||||||
connectionStatus = 'checking';
|
connectionStatus = 'checking';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** True only when the browser is online and the last active-server probe succeeded. */
|
/** True only when no navigator offline hint applies and the last active-server probe succeeded. */
|
||||||
export function isActiveServerReachable(): boolean {
|
export function isActiveServerReachable(): boolean {
|
||||||
if (isDevOfflineBrowseForced()) return false;
|
if (isDevOfflineBrowseForced()) return false;
|
||||||
if (typeof navigator !== 'undefined' && !navigator.onLine) return false;
|
if (isNavigatorOfflineHint()) return false;
|
||||||
return activeServerReachable === true;
|
return activeServerReachable === true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
|
||||||
|
const isTauri = vi.fn(() => false);
|
||||||
|
|
||||||
|
vi.mock('@tauri-apps/api/core', () => ({
|
||||||
|
isTauri: () => isTauri(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { isNavigatorOfflineHint } from './navigatorOnlineHint';
|
||||||
|
|
||||||
|
describe('isNavigatorOfflineHint', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
isTauri.mockReturnValue(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false in Tauri even when navigator.onLine is false', () => {
|
||||||
|
isTauri.mockReturnValue(true);
|
||||||
|
vi.stubGlobal('navigator', { onLine: false });
|
||||||
|
expect(isNavigatorOfflineHint()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true in non-Tauri when navigator.onLine is false (offline hint applies)', () => {
|
||||||
|
vi.stubGlobal('navigator', { onLine: false });
|
||||||
|
expect(isNavigatorOfflineHint()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false when navigator.onLine is true', () => {
|
||||||
|
vi.stubGlobal('navigator', { onLine: true });
|
||||||
|
expect(isNavigatorOfflineHint()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { isTauri } from '@tauri-apps/api/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether callers should treat `navigator.onLine === false` as an offline signal.
|
||||||
|
*
|
||||||
|
* Returns `true` only in non-Tauri hosts when `navigator.onLine` is false.
|
||||||
|
* WebKitGTK inside Tauri often leaves `navigator.onLine === false` even when
|
||||||
|
* HTTP to the user's Subsonic/Navidrome server works (ping, search, playback).
|
||||||
|
* Desktop builds must not trust that hint — use Subsonic probes instead.
|
||||||
|
*
|
||||||
|
* @see https://github.com/orgs/tauri-apps/discussions/9269
|
||||||
|
*/
|
||||||
|
export function isNavigatorOfflineHint(): boolean {
|
||||||
|
if (typeof navigator === 'undefined') return false;
|
||||||
|
try {
|
||||||
|
if (isTauri()) return false;
|
||||||
|
} catch {
|
||||||
|
/* isTauri unavailable in some test harnesses — fall through */
|
||||||
|
}
|
||||||
|
return !navigator.onLine;
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { hasLocalPlaybackUrl } from '@/store/localPlaybackResolve';
|
|||||||
import { isDevOfflineBrowseForced } from '@/store/devOfflineBrowseStore';
|
import { isDevOfflineBrowseForced } from '@/store/devOfflineBrowseStore';
|
||||||
import { resolveServerIdForIndexKey } from '@/lib/server/serverLookup';
|
import { resolveServerIdForIndexKey } from '@/lib/server/serverLookup';
|
||||||
import { isActiveServerReachable } from '@/lib/network/activeServerReachability';
|
import { isActiveServerReachable } from '@/lib/network/activeServerReachability';
|
||||||
|
import { isNavigatorOfflineHint } from '@/lib/network/navigatorOnlineHint';
|
||||||
|
|
||||||
function isSameServerProfile(a: string, b: string): boolean {
|
function isSameServerProfile(a: string, b: string): boolean {
|
||||||
if (!a || !b) return false;
|
if (!a || !b) return false;
|
||||||
@@ -18,7 +19,7 @@ function isSameServerProfile(a: string, b: string): boolean {
|
|||||||
export function isSubsonicServerReachable(serverId: string): boolean {
|
export function isSubsonicServerReachable(serverId: string): boolean {
|
||||||
if (!serverId) return false;
|
if (!serverId) return false;
|
||||||
if (isDevOfflineBrowseForced()) return false;
|
if (isDevOfflineBrowseForced()) return false;
|
||||||
if (typeof navigator !== 'undefined' && !navigator.onLine) return false;
|
if (isNavigatorOfflineHint()) return false;
|
||||||
const activeId = useAuthStore.getState().activeServerId;
|
const activeId = useAuthStore.getState().activeServerId;
|
||||||
if (!activeId || isSameServerProfile(serverId, activeId)) {
|
if (!activeId || isSameServerProfile(serverId, activeId)) {
|
||||||
return isActiveServerReachable();
|
return isActiveServerReachable();
|
||||||
@@ -34,7 +35,7 @@ export function isSubsonicServerReachable(serverId: string): boolean {
|
|||||||
export function shouldAttemptSubsonicForServer(serverId: string, trackId?: string): boolean {
|
export function shouldAttemptSubsonicForServer(serverId: string, trackId?: string): boolean {
|
||||||
if (!serverId) return false;
|
if (!serverId) return false;
|
||||||
if (isDevOfflineBrowseForced()) return false;
|
if (isDevOfflineBrowseForced()) return false;
|
||||||
if (typeof navigator !== 'undefined' && !navigator.onLine) return false;
|
if (isNavigatorOfflineHint()) return false;
|
||||||
if (trackId && hasLocalPlaybackUrl(trackId, serverId)) return false;
|
if (trackId && hasLocalPlaybackUrl(trackId, serverId)) return false;
|
||||||
return isSubsonicServerReachable(serverId);
|
return isSubsonicServerReachable(serverId);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user