Load newest albums and complete song pages, add a Songs view, bundle FFmpeg, persist credentials securely, and serve the managed library on the LAN

Hop-State: A_06FNEJ4B52YWW6V4DQKVHXR
Hop-Proposal: R_06FNEJ37KHGP1VCKVNTV95R
Hop-Task: T_06FNEEBRXR47KT7H71N5H68
Hop-Attempt: AT_06FNEEBRXS5YEZ0KZ4MTDZG
This commit is contained in:
2026-07-12 10:03:19 -07:00
committed by Hop
parent 0168767819
commit 73c4eb1857
17 changed files with 1191 additions and 58 deletions
+27 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it, vi } from "vitest";
import { pollForLibrary } from "./librarySync";
import { fetchAllPages, pollForLibrary } from "./librarySync";
describe("pollForLibrary", () => {
it("waits until Navidrome returns indexed tracks", async () => {
@@ -24,4 +24,30 @@ describe("pollForLibrary", () => {
await expect(pollForLibrary(load, { attempts: 3, wait })).resolves.toBeNull();
expect(load).toHaveBeenCalledTimes(3);
});
it("can wait for albums and tracks to finish indexing together", async () => {
const load = vi
.fn()
.mockResolvedValueOnce({ albums: [], tracks: [{ id: "early" }] })
.mockResolvedValueOnce({ albums: [{ id: "album" }], tracks: [{ id: "ready" }] });
await expect(pollForLibrary<{ albums: { id: string }[]; tracks: { id: string }[] }>(load, {
attempts: 2,
wait: async () => undefined,
isReady: (snapshot) => snapshot.albums.length > 0 && snapshot.tracks.length > 0,
})).resolves.toEqual({ albums: [{ id: "album" }], tracks: [{ id: "ready" }] });
});
});
describe("fetchAllPages", () => {
it("loads successive pages until the server returns a partial page", async () => {
const loadPage = vi
.fn()
.mockResolvedValueOnce(["one", "two"])
.mockResolvedValueOnce(["three"]);
await expect(fetchAllPages(loadPage, 2)).resolves.toEqual(["one", "two", "three"]);
expect(loadPage).toHaveBeenNthCalledWith(1, 0, 2);
expect(loadPage).toHaveBeenNthCalledWith(2, 2, 2);
});
});
+23 -3
View File
@@ -2,22 +2,42 @@ export interface LibrarySnapshot<TTrack> {
tracks: readonly TTrack[];
}
interface PollOptions {
export async function fetchAllPages<T>(
loadPage: (offset: number, size: number) => Promise<readonly T[]>,
pageSize = 500,
maxPages = 100,
): Promise<T[]> {
const items: T[] = [];
for (let page = 0; page < maxPages; page += 1) {
const batch = await loadPage(page * pageSize, pageSize);
items.push(...batch);
if (batch.length < pageSize) break;
}
return items;
}
interface PollOptions<TSnapshot> {
attempts?: number;
delayMs?: number;
wait?: (delayMs: number) => Promise<void>;
isReady?: (snapshot: TSnapshot) => boolean;
}
const waitFor = (delayMs: number) => new Promise<void>((resolve) => window.setTimeout(resolve, delayMs));
export async function pollForLibrary<T extends LibrarySnapshot<unknown>>(
load: () => Promise<T>,
{ attempts = 120, delayMs = 1_000, wait = waitFor }: PollOptions = {},
{
attempts = 120,
delayMs = 1_000,
wait = waitFor,
isReady = (snapshot) => snapshot.tracks.length > 0,
}: PollOptions<T> = {},
): Promise<T | null> {
for (let attempt = 0; attempt < attempts; attempt += 1) {
try {
const snapshot = await load();
if (snapshot.tracks.length > 0) return snapshot;
if (isReady(snapshot)) return snapshot;
} catch (error) {
if (attempt === attempts - 1) throw error;
}
+6 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { getManagedServerStatus, isDesktopRuntime } from "./managedServer";
import { getManagedServerStatus, getSavedCredentials, isDesktopRuntime } from "./managedServer";
describe("managed server bridge", () => {
it("reports the browser preview as a non-desktop runtime", () => {
@@ -10,7 +10,12 @@ describe("managed server bridge", () => {
await expect(getManagedServerStatus()).resolves.toMatchObject({
running: false,
url: "http://127.0.0.1:4533",
lanUrls: [],
version: "0.61.2",
});
});
it("does not expose credentials in the browser preview", async () => {
await expect(getSavedCredentials()).resolves.toEqual({});
});
});
+17 -1
View File
@@ -4,10 +4,16 @@ import { open } from "@tauri-apps/plugin-dialog";
export interface ManagedServerStatus {
running: boolean;
url: string;
lanUrls: string[];
musicFolder?: string;
version: string;
}
export interface SavedCredentials {
localPassword?: string;
remotePassword?: string;
}
export function isDesktopRuntime(): boolean {
return typeof window !== "undefined" && "__TAURI_INTERNALS__" in window;
}
@@ -24,11 +30,21 @@ export async function chooseMusicFolder(): Promise<string | null> {
export async function getManagedServerStatus(): Promise<ManagedServerStatus> {
if (!isDesktopRuntime()) {
return { running: false, url: "http://127.0.0.1:4533", version: "0.61.2" };
return { running: false, url: "http://127.0.0.1:4533", lanUrls: [], version: "0.61.2" };
}
return invoke<ManagedServerStatus>("managed_server_status");
}
export async function getSavedCredentials(): Promise<SavedCredentials> {
if (!isDesktopRuntime()) return {};
return invoke<SavedCredentials>("load_saved_credentials");
}
export async function saveRemotePassword(password: string): Promise<void> {
if (!isDesktopRuntime()) return;
await invoke("save_remote_password", { password });
}
export async function startManagedServer(musicFolder: string, password: string): Promise<ManagedServerStatus> {
if (!isDesktopRuntime()) throw new Error("The managed library is available in the Resonant desktop app.");
return invoke<ManagedServerStatus>("start_managed_server", { musicFolder, password });