Fix Jellyfin Login (#76)

This commit is contained in:
2026-05-20 00:46:11 -07:00
committed by GitHub
parent 799cce6991
commit a54f03f0cd
31 changed files with 1087 additions and 148 deletions
+34
View File
@@ -654,6 +654,40 @@ test('authenticateWithPassword surfaces invalid credentials and server status fa
}
});
test('authenticateWithPassword surfaces unreachable server failures', async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = (async () => {
throw new TypeError('fetch failed');
}) as typeof fetch;
try {
await assert.rejects(
() => authenticateWithPassword('http://jellyfin.local:8096/', 'kyle', 'pw', clientInfo),
/Could not reach Jellyfin server \(fetch failed\)\./,
);
} finally {
globalThis.fetch = originalFetch;
}
});
test('authenticateWithPassword surfaces login timeouts', async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = (async () => {
const error = new Error('aborted') as Error & { name: string };
error.name = 'AbortError';
throw error;
}) as typeof fetch;
try {
await assert.rejects(
() => authenticateWithPassword('http://jellyfin.local:8096/', 'kyle', 'pw', clientInfo),
/Jellyfin login timed out\./,
);
} finally {
globalThis.fetch = originalFetch;
}
});
test('listLibraries surfaces token-expiry auth errors', async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = (async () =>
+40 -11
View File
@@ -1,6 +1,7 @@
import { JellyfinConfig } from '../../types';
const JELLYFIN_TICKS_PER_SECOND = 10_000_000;
const JELLYFIN_LOGIN_TIMEOUT_MS = 15_000;
export interface JellyfinAuthSession {
serverUrl: string;
@@ -116,6 +117,21 @@ function asIntegerOrNull(value: unknown): number | null {
return typeof value === 'number' && Number.isInteger(value) ? value : null;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}
function isAbortError(error: unknown): boolean {
return isRecord(error) && error.name === 'AbortError';
}
function getErrorMessage(error: unknown): string {
if (error instanceof Error && error.message) {
return error.message;
}
return String(error || 'unknown error');
}
function resolveDeliveryUrl(
session: JellyfinAuthSession,
stream: JellyfinMediaStream,
@@ -309,17 +325,30 @@ export async function authenticateWithPassword(
if (!username.trim()) throw new Error('Missing Jellyfin username.');
if (!password) throw new Error('Missing Jellyfin password.');
const response = await fetch(`${normalizedUrl}/Users/AuthenticateByName`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: createAuthorizationHeader(client),
},
body: JSON.stringify({
Username: username,
Pw: password,
}),
});
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), JELLYFIN_LOGIN_TIMEOUT_MS);
let response: Response;
try {
response = await fetch(`${normalizedUrl}/Users/AuthenticateByName`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: createAuthorizationHeader(client),
},
body: JSON.stringify({
Username: username,
Pw: password,
}),
signal: controller.signal,
});
} catch (error) {
if (isAbortError(error)) {
throw new Error('Jellyfin login timed out. Check the server URL and network connection.');
}
throw new Error(`Could not reach Jellyfin server (${getErrorMessage(error)}).`);
} finally {
clearTimeout(timeout);
}
if (response.status === 401 || response.status === 403) {
throw new Error('Invalid Jellyfin username or password.');