Something went wrong. Try again.
a simple but full-featured web player for OpenSubsonic music servers (Navidrome, Gonic, Ampache, etc) devins.page/tinysub
svelte typescript javascript subsonic gonic navidrome
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310import { createToken, detectExtensions, getSourceApi, isAuthenticationError, isNetworkError,} from "./client.svelte.js";import type { Credentials } from "./credentials.js";import { deploymentConfig } from "./deploymentConfig.js";import { loadLib } from "./library.svelte.js";import { offlineManager } from "./offline/offlineManager.svelte.js";import { updateBackgroundSync } from "./pwa.svelte.js";import { removeSourceTracks, syncQueue } from "./queue.svelte.js";import { effectiveAutoSyncMode } from "./settings.svelte.js";import { addSource, getSource, removeSource, setActiveSource, sources, updateSource,} from "./sources.svelte.js";
export const auth = $state({ sourceId: null as string | null, loggedIn: false, busy: false, err: null as string | null, user: "", admin: false, authFailed: false, autoLogin: false,});
const setAuthSource = (sourceId: string | null, loggedIn = !!sourceId) => { const source = getSource(sourceId); Object.assign(auth, { sourceId, loggedIn: loggedIn && !!source, user: source?.credentials.username || "", admin: false, err: null, authFailed: false, });};
const isCurrentSource = (sourceId: string) => sourceId === sources.activeId && auth.sourceId === sourceId;
export const markAuthFailed = () => { Object.assign(auth, { loggedIn: false, admin: false, autoLogin: false, authFailed: true, }); offlineManager.setOffline();};
const loadSessionData = async (sourceId: string, includeQueue = false) => { const source = getSource(sourceId); if (!source) return; let admin = false; const tasks: Promise<unknown>[] = [loadLib(sourceId)]; if (includeQueue) tasks.push(syncQueue(sourceId)); if (source.credentials.username) { tasks.push( getSourceApi(sourceId) .getUser(source.credentials.username) .then((user) => { admin = user.adminRole === true || user.adminRole === "true"; }) .catch((err) => { console.error("failed to fetch user info:", err); }), ); } await Promise.all(tasks); if (isCurrentSource(sourceId)) auth.admin = admin;};
const activateLoadedSource = async ( sourceId: string, includeQueue = false, checkConnection = false,) => { if (!getSource(sourceId) || !setActiveSource(sourceId)) return false; setAuthSource(sourceId); await offlineManager.setSource(sourceId); if (checkConnection) { if (!isCurrentSource(sourceId)) return false; await getSourceApi(sourceId).forcePing(); if (!isCurrentSource(sourceId)) return false; offlineManager.setConnected(sourceId); } await detectExtensions(sourceId); if (checkConnection && !isCurrentSource(sourceId)) return false; await loadSessionData(sourceId, includeQueue); return true;};
export const activateSource = async (sourceId: string) => { if (sourceId === sources.activeId && auth.loggedIn) return true; auth.autoLogin = false; try { return await activateLoadedSource( sourceId, false, offlineManager.isNetworkAvailable, ); } catch (err) { if (!isCurrentSource(sourceId)) return false; if (isAuthenticationError(err)) markAuthFailed(); else console.error("failed to activate library:", err); try { await loadSessionData(sourceId); } catch (offlineErr) { console.error("failed to load cached library:", offlineErr); } return false; }};
export const login = async ( serverUrl: string, username: string, secret: string, authMode: "token" | "password" | "apikey" | "auto" = "auto", serverName = "",) => { if (deploymentConfig.serverLock && deploymentConfig.server) serverUrl = deploymentConfig.server; if (authMode === "apikey") username = ""; serverUrl = serverUrl.trim().replace(/\/+$/, ""); username = username.trim(); const previousSourceId = sources.activeId; const previousAuthState = { sourceId: auth.sourceId, loggedIn: auth.loggedIn, user: auth.user, admin: auth.admin, authFailed: auth.authFailed, }; const wasOffline = offlineManager.isOfflineMode; auth.busy = true; auth.err = null; auth.authFailed = false; const source = addSource(serverName, { server: serverUrl, authMode: authMode === "auto" ? "token" : authMode, username, });
try { const tryAuth = async (mode: "token" | "password" | "apikey") => { const credentials: Credentials = { server: serverUrl, authMode: mode, ...(mode === "apikey" ? { apiKey: secret } : mode === "password" ? { username, password: secret } : { username, ...(await createToken(secret)) }), }; updateSource(source.id, { credentials }); await getSourceApi(source.id).forcePing(); };
if (authMode === "auto") { try { await tryAuth("token"); } catch (err) { if (isNetworkError(err)) throw err; console.warn("token auth failed, trying cleartext:", err); await tryAuth("password"); } } else { await tryAuth(authMode); }
offlineManager.setConnected(source.id); await detectExtensions(source.id); await offlineManager.autoSyncQueuedRequests();
if (authMode === "apikey") { try { const info = await getSourceApi(source.id).tokenInfo(); const user = info?.tokenInfo?.username; if (user) { username = user; const current = getSource(source.id); if (current) updateSource(source.id, { credentials: { ...current.credentials, username }, }); } } catch (err) { console.warn("failed to fetch token info:", err); } }
await activateLoadedSource(source.id); return true; } catch (err) { const errorMessage = err instanceof Error ? err.message : String(err); auth.err = errorMessage; removeSource(source.id); updateBackgroundSync(effectiveAutoSyncMode()); if (previousSourceId) { setActiveSource(previousSourceId); if (wasOffline) offlineManager.setOffline(); await offlineManager.setSource(previousSourceId); } else { offlineManager.setOffline(); await loadLib(null); } Object.assign(auth, previousAuthState, { err: errorMessage }); return false; } finally { auth.busy = false; auth.autoLogin = false; }};
export const removeLibrarySource = async (sourceId: string) => { if (!getSource(sourceId)) return false; const wasActive = sources.activeId === sourceId; try { await offlineManager.removeSourceData(sourceId); } catch (err) { console.error("failed to clear library data:", err); return false; } removeSourceTracks(sourceId); removeSource(sourceId); updateBackgroundSync(effectiveAutoSyncMode()); if (wasActive) { if (sources.activeId) await tryStoredSession(sources.activeId); else { setAuthSource(null, false); offlineManager.setOffline(); await loadLib(null); } } return true;};
export const isOwner = (owner?: string | null) => !owner || (!!auth.user && owner.toLowerCase() === auth.user.toLowerCase());
const tryStoredSession = async (sourceId: string) => { if (!getSource(sourceId) || !setActiveSource(sourceId)) return false; setAuthSource(sourceId); auth.autoLogin = true; offlineManager.setOffline(); try { await offlineManager.setSource(sourceId); if (!isCurrentSource(sourceId)) return false; await getSourceApi(sourceId).forcePing(); if (!isCurrentSource(sourceId)) return false; offlineManager.setConnected(); await detectExtensions(sourceId); if (!isCurrentSource(sourceId)) return false; await offlineManager.autoSyncQueuedRequests(); if (!isCurrentSource(sourceId)) return false; await loadSessionData(sourceId, true); if (!isCurrentSource(sourceId)) return false; return true; } catch (err) { if (!isCurrentSource(sourceId)) return false; if (isAuthenticationError(err)) markAuthFailed(); else if (isNetworkError(err)) auth.err = null; else console.error("failed to restore stored library:", err); try { if (isCurrentSource(sourceId)) await loadSessionData(sourceId, true); } catch (offlineErr) { console.error("failed to load cached library:", offlineErr); } return false; } finally { if (isCurrentSource(sourceId)) auth.autoLogin = false; }};
export const initializeAuth = async () => { if (sources.primaryId) await tryStoredSession(sources.primaryId);};
export const reconnectSession = async (sourceId = auth.sourceId) => { if (!sourceId || !getSource(sourceId) || !isCurrentSource(sourceId)) return; try { await getSourceApi(sourceId).forcePing(); if (!isCurrentSource(sourceId)) return; offlineManager.setConnected(); await detectExtensions(sourceId); if (!isCurrentSource(sourceId)) return; await offlineManager.autoSyncQueuedRequests(); if (!isCurrentSource(sourceId)) return; await loadSessionData(sourceId); if (isCurrentSource(sourceId)) setAuthSource(sourceId); } catch (err) { if (isAuthenticationError(err) && isCurrentSource(sourceId)) markAuthFailed(); else console.error("failed to reconnect library:", err); }};
export const retrySession = (sourceId = auth.sourceId) => sourceId ? tryStoredSession(sourceId) : Promise.resolve(false);