diff --git a/emby.json b/emby.json index 5b6a130..874b253 100644 --- a/emby.json +++ b/emby.json @@ -1,8 +1,8 @@ { "version": 6, "script": { - "version": "2.1.0", - "changeLog": "Added the now-playing widget and moved to the v6 install script format" + "version": "2.2.0", + "changeLog": "Setup is optional: a prompt left unanswered no longer fails the install, and Pre-configure Emby can be run from the app later" }, "requirements": { "locations": ["ApplicationsPerformance", "ApplicationsCapacity", "Media", "Photos", "Music", "Movies", "Shows", "Videos"], @@ -79,12 +79,19 @@ { "id": "configure-emby", "title": "Pre-configure Emby", - "events": ["onAfterInstall"], + "events": [ + "onAfterInstall", + "userAction" + ], + "rerun": "converge", + "surfaces": [ + "card" + ], "script": "emby/emby_hook.ts", "entrypoint": "afterInstall", "timeout": 300, "description": "Setting up Emby", - "optional": false, + "optional": true, "retries": 0, "userOptional": { "description": "Automatically set up your server, apply preferences, and create media libraries. By enabling this, you agree that HexOS will accept the Emby Terms of Service on your behalf.", diff --git a/emby/emby_hook.ts b/emby/emby_hook.ts index 85c68f7..ae07b66 100644 --- a/emby/emby_hook.ts +++ b/emby/emby_hook.ts @@ -53,7 +53,11 @@ export async function afterInstall(ctx: HookContext) { token = await authenticate(ctx, username, password); } catch (e) { if (alreadyConfigured) { - throw new Error("Existing Emby data detected. Please retry pre-configuration with the original username and password, or skip to keep your previous install."); + throw new Error( + ctx.event === "action" + ? "Existing Emby data detected. Run Pre-configure Emby again with the original username and password." + : "Existing Emby data detected. Please retry pre-configuration with the original username and password, or skip to keep your previous install.", + ); } throw e; } @@ -218,13 +222,14 @@ const LIBRARIES = [ { name: "Videos", collectionType: "homevideos", path: "/videos", checkpointId: "lib_videos" }, ]; -async function getExistingLibraryPaths(ctx: HookContext, token: string): Promise> { +/** The paths of the server's libraries, or null when they could not be read. */ +async function getExistingLibraryPaths(ctx: HookContext, token: string): Promise | null> { try { const resp = await fetch(api(ctx, "/Library/VirtualFolders"), { headers: authHeader(token), signal: AbortSignal.timeout(10000), }); - if (!resp.ok) return new Set(); + if (!resp.ok) return null; const data = await resp.json() as Array<{ Locations?: string[] }>; const paths = new Set(); for (const lib of data) { @@ -232,7 +237,7 @@ async function getExistingLibraryPaths(ctx: HookContext, token: string): Promise } return paths; } catch { - return new Set(); + return null; } } @@ -240,7 +245,13 @@ async function createLibraries(ctx: HookContext, token: string): Promise { await waitForLibrarySubsystem(ctx, token); await ctx.sleep(LIBRARY_PACE_MS); - const existingPaths = await getExistingLibraryPaths(ctx, token); + const listed = await getExistingLibraryPaths(ctx, token); + // A run from the app card is on a server that may already have its libraries: libraries it + // cannot list are not assumed missing, or it would make them again. + if (listed === null && ctx.event === "action") { + throw new Error("Could not read the server's libraries, so none were created. Run it again."); + } + const existingPaths = listed ?? new Set(); for (let i = 0; i < LIBRARIES.length; i++) { const lib = LIBRARIES[i]; @@ -289,7 +300,7 @@ async function createLibraries(ctx: HookContext, token: string): Promise { if (resp.ok) { await ctx.sleep(2000); const verified = await getExistingLibraryPaths(ctx, token); - if (verified.has(lib.path)) { + if (verified?.has(lib.path)) { ctx.log(`Created library: ${lib.name}`); created = true; break; @@ -313,6 +324,11 @@ async function createLibraries(ctx: HookContext, token: string): Promise { await ctx.emitCheckpoint(lib.checkpointId, `${lib.name} — created`); } else { await ctx.updateCheckpointMessage(lib.checkpointId, `${lib.name} — failed`); + // A run from the app card has no retry or skip to wait on: it stops here, and running it + // again carries on from the libraries already made. + if (ctx.event === "action") { + throw new Error(`Could not create the ${lib.name} library. Run it again to retry it.`); + } const action = await ctx.awaitCheckpointRetry( lib.checkpointId, `Failed to create library: ${lib.name}`, diff --git a/jellyfin.json b/jellyfin.json index 3867ad3..239b0d6 100644 --- a/jellyfin.json +++ b/jellyfin.json @@ -1,8 +1,8 @@ { "version": 6, "script": { - "version": "2.1.0", - "changeLog": "Added the now-playing widget and moved to the v6 install script format" + "version": "2.2.0", + "changeLog": "Setup is optional: a prompt left unanswered no longer fails the install, and Pre-configure Jellyfin can be run from the app later" }, "requirements": { "locations": ["ApplicationsPerformance", "ApplicationsCapacity", "Media", "Photos", "Music", "Movies", "Shows", "Videos"], @@ -51,13 +51,18 @@ "id": "configure-jellyfin", "title": "Pre-configure Jellyfin", "events": [ - "onAfterInstall" + "onAfterInstall", + "userAction" + ], + "rerun": "converge", + "surfaces": [ + "card" ], "script": "jellyfin/jellyfin_hook.ts", "entrypoint": "afterInstall", "timeout": 300, "description": "Setting up Jellyfin", - "optional": false, + "optional": true, "retries": 0, "userOptional": { "description": "Automatically set up your server, apply preferences, and create media libraries. By enabling this, you acknowledge Jellyfin's media server policy.", diff --git a/jellyfin/jellyfin_hook.ts b/jellyfin/jellyfin_hook.ts index 3f566ef..9f1709e 100644 --- a/jellyfin/jellyfin_hook.ts +++ b/jellyfin/jellyfin_hook.ts @@ -48,7 +48,11 @@ export async function afterInstall(ctx: HookContext) { token = await authenticate(ctx, username, password); } catch (e) { if (alreadyConfigured) { - throw new Error("Existing Jellyfin data detected. Please retry pre-configuration with the original username and password, or skip to keep your previous install."); + throw new Error( + ctx.event === "action" + ? "Existing Jellyfin data detected. Run Pre-configure Jellyfin again with the original username and password." + : "Existing Jellyfin data detected. Please retry pre-configuration with the original username and password, or skip to keep your previous install.", + ); } throw e; } @@ -244,13 +248,14 @@ const LIBRARIES = [ { name: "Videos", collectionType: "homevideos", path: "/videos", checkpointId: "lib_videos" }, ]; -async function getExistingLibraryPaths(ctx: HookContext, token: string): Promise> { +/** The paths of the server's libraries, or null when they could not be read. */ +async function getExistingLibraryPaths(ctx: HookContext, token: string): Promise | null> { try { const resp = await fetch(`${ctx.baseUrl}/Library/VirtualFolders`, { headers: { "Authorization": `MediaBrowser Token="${token}"` }, signal: AbortSignal.timeout(10000), }); - if (!resp.ok) return new Set(); + if (!resp.ok) return null; const data = await resp.json() as Array<{ Locations?: string[] }>; const paths = new Set(); for (const lib of data) { @@ -258,7 +263,7 @@ async function getExistingLibraryPaths(ctx: HookContext, token: string): Promise } return paths; } catch { - return new Set(); + return null; } } @@ -266,7 +271,13 @@ async function createLibraries(ctx: HookContext, token: string): Promise { await waitForLibrarySubsystem(ctx, token); await ctx.sleep(LIBRARY_PACE_MS); - const existingPaths = await getExistingLibraryPaths(ctx, token); + const listed = await getExistingLibraryPaths(ctx, token); + // A run from the app card is on a server that may already have its libraries: libraries it + // cannot list are not assumed missing, or it would make them again. + if (listed === null && ctx.event === "action") { + throw new Error("Could not read the server's libraries, so none were created. Run it again."); + } + const existingPaths = listed ?? new Set(); for (let i = 0; i < LIBRARIES.length; i++) { const lib = LIBRARIES[i]; @@ -311,7 +322,7 @@ async function createLibraries(ctx: HookContext, token: string): Promise { if (resp.ok) { await ctx.sleep(2000); const verified = await getExistingLibraryPaths(ctx, token); - if (verified.has(lib.path)) { + if (verified?.has(lib.path)) { ctx.log(`Created library: ${lib.name}`); created = true; break; @@ -335,6 +346,11 @@ async function createLibraries(ctx: HookContext, token: string): Promise { await ctx.emitCheckpoint(lib.checkpointId, `${lib.name} — created`); } else { await ctx.updateCheckpointMessage(lib.checkpointId, `${lib.name} — failed`); + // A run from the app card has no retry or skip to wait on: it stops here, and running it + // again carries on from the libraries already made. + if (ctx.event === "action") { + throw new Error(`Could not create the ${lib.name} library. Run it again to retry it.`); + } const action = await ctx.awaitCheckpointRetry( lib.checkpointId, `Failed to create library: ${lib.name}`,