Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions emby.json
Original file line number Diff line number Diff line change
@@ -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"],
Expand Down Expand Up @@ -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.",
Expand Down
28 changes: 22 additions & 6 deletions emby/emby_hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -218,29 +222,36 @@ const LIBRARIES = [
{ name: "Videos", collectionType: "homevideos", path: "/videos", checkpointId: "lib_videos" },
];

async function getExistingLibraryPaths(ctx: HookContext, token: string): Promise<Set<string>> {
/** The paths of the server's libraries, or null when they could not be read. */
async function getExistingLibraryPaths(ctx: HookContext, token: string): Promise<Set<string> | 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<string>();
for (const lib of data) {
for (const loc of lib.Locations ?? []) paths.add(loc);
}
return paths;
} catch {
return new Set();
return null;
}
}

async function createLibraries(ctx: HookContext, token: string): Promise<void> {
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<string>();

for (let i = 0; i < LIBRARIES.length; i++) {
const lib = LIBRARIES[i];
Expand Down Expand Up @@ -289,7 +300,7 @@ async function createLibraries(ctx: HookContext, token: string): Promise<void> {
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;
Expand All @@ -313,6 +324,11 @@ async function createLibraries(ctx: HookContext, token: string): Promise<void> {
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}`,
Expand Down
13 changes: 9 additions & 4 deletions jellyfin.json
Original file line number Diff line number Diff line change
@@ -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"],
Expand Down Expand Up @@ -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.",
Expand Down
28 changes: 22 additions & 6 deletions jellyfin/jellyfin_hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -244,29 +248,36 @@ const LIBRARIES = [
{ name: "Videos", collectionType: "homevideos", path: "/videos", checkpointId: "lib_videos" },
];

async function getExistingLibraryPaths(ctx: HookContext, token: string): Promise<Set<string>> {
/** The paths of the server's libraries, or null when they could not be read. */
async function getExistingLibraryPaths(ctx: HookContext, token: string): Promise<Set<string> | 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<string>();
for (const lib of data) {
for (const loc of lib.Locations ?? []) paths.add(loc);
}
return paths;
} catch {
return new Set();
return null;
}
}

async function createLibraries(ctx: HookContext, token: string): Promise<void> {
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<string>();

for (let i = 0; i < LIBRARIES.length; i++) {
const lib = LIBRARIES[i];
Expand Down Expand Up @@ -311,7 +322,7 @@ async function createLibraries(ctx: HookContext, token: string): Promise<void> {
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;
Expand All @@ -335,6 +346,11 @@ async function createLibraries(ctx: HookContext, token: string): Promise<void> {
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}`,
Expand Down
Loading