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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185<script lang="ts">import { slide } from "svelte/transition";import { auth, getSessionCredentials, login } from "../api/auth.svelte.js";import { asArray } from "../api/client.svelte.js";import { anonymousGetExtensions, anonymousPing, deploymentConfig,} from "../api/connection.js";
import { t } from "../languages/translate.svelte.js";import { entryMotion, motionEnabled } from "../ui/motion.svelte.js";import Modal from "./Modal.svelte";
const saved = getSessionCredentials();
let server = $state(saved?.server || deploymentConfig.server), authMode = $state<"auto" | "apikey">( saved?.authMode === "apikey" ? "apikey" : "auto", ), user = $state(saved?.username || deploymentConfig.username), password = $state( saved?.password || saved?.apiKey || deploymentConfig.password, );
let pingStatus = $state<"none" | "loading" | "ok" | "fail">("none");let isApiKeySupported = $state(saved?.authMode === "apikey");
$effect(() => { if (!server.trim()) { pingStatus = "none"; isApiKeySupported = false; authMode = "auto"; return; }
pingStatus = "loading"; isApiKeySupported = false;
const requestServer = server; const timer = setTimeout(async () => { try { const data = await anonymousPing(requestServer); if (server !== requestServer) return; const isSubsonic = data && (data.status === "ok" || data.status === "failed"); pingStatus = isSubsonic ? "ok" : "fail";
if (isSubsonic && data.openSubsonic) { try { const extRes = await anonymousGetExtensions(requestServer); if (server !== requestServer) return; const list = asArray( extRes.openSubsonicExtensions?.extension || extRes.openSubsonicExtensions, ); isApiKeySupported = list.some( (e: { name?: string }) => e?.name?.toLowerCase() === "apikeyauthentication", ); } catch (err) { console.warn("failed to fetch extensions anonymously:", err); } } } catch { if (server !== requestServer) return; pingStatus = "fail"; }
if (!isApiKeySupported && authMode === "apikey") { authMode = "auto"; } }, 500);
return () => clearTimeout(timer);});</script>
<Modal open={!auth.loggedIn && !auth.autoLogin && !auth.authFailed} title={t("auth.login")} closable={false}> <form onsubmit={(e) => { e.preventDefault(); login(server, authMode === "apikey" ? "" : user, password, authMode); }} > {#if !deploymentConfig.serverLock} <input id="auth-server" name="server" bind:value={server} placeholder={t("auth.server_placeholder")} required autocomplete="url" > {/if}
{#if pingStatus !== "none"} <div class:ok={pingStatus === "ok"} class:fail={pingStatus === "fail"} transition:slide={{ duration: motionEnabled() ? entryMotion.duration : 0, }} > {#if pingStatus === "loading"} ... {:else if pingStatus === "ok"} {t("auth.server_found")} {:else} {t("auth.server_not_found")} - <a target="_blank" href="https://tangled.org/devins.page/tinysub#i-cant-log-in" >{t("auth.login_notice")} </a> {/if} </div> {/if}
{#if isApiKeySupported} <select id="auth-mode" name="authMode" bind:value={authMode}> <option value="auto">{t("auth.username_password")}</option> <option value="apikey">{t("auth.apikey")}</option> </select> {/if}
{#if authMode !== "apikey"} <input id="auth-username" name="username" bind:value={user} placeholder={t("auth.username_placeholder")} required autocomplete="username" > {/if} <input id="auth-password" name="password" bind:value={password} type="password" placeholder={authMode === "apikey" ? t("auth.apikey") : t("auth.password_placeholder")} required autocomplete={authMode === "apikey" ? "off" : "current-password"} > <button class="native" type="submit" disabled={auth.busy}> {auth.busy ? t("auth.logging_in") : t("auth.login")} </button> {#if auth.err} <div class="fail" transition:slide={{ duration: motionEnabled() ? entryMotion.duration : 0, }} > {auth.err} </div> {/if} </form></Modal>
<style>form { inline-size: 22rem; max-inline-size: 100%; padding: 1rem;}form > div { font-size: smaller;}.ok { color: green;}.fail { color: red;}</style>