fix(jellyfin): fix discovery loop, device identity, tray state, and Disc

- Derive device identity from OS hostname; remove legacy configurable client/device fields
- Prevent discovery playback from reloading active item, misreporting pause state, and duplicate overlay restores
- Restart stale tray discovery sessions without re-login when server drops SubMiner cast target
- Sync tray discovery checkbox state on Linux after CLI/startup/remote-session changes
- Stop Discord presence falling back to stream URLs; prime title before tokenized stream loads
- Fix picker library discovery when log level is above info
- Fix config.example.jsonc trailing commas and array formatting
This commit is contained in:
2026-05-22 01:36:11 -07:00
parent e17c499cfe
commit f19d93e3ab
72 changed files with 1902 additions and 295 deletions
@@ -91,6 +91,22 @@ test('buildDiscordPresenceActivity shows media title regardless of style', () =>
}
});
test('buildDiscordPresenceActivity never falls back to remote stream URLs', () => {
const payload = buildDiscordPresenceActivity(baseConfig, {
...baseSnapshot,
mediaTitle: null,
mediaPath:
'http://jellyfin.local/Videos/item-1/stream?static=true&api_key=secret-token&MediaSourceId=ms-1',
});
assert.equal(payload.details, 'Unknown media');
assert.equal(payload.state, 'Playing 01:35 / 24:10');
const serialized = JSON.stringify(payload);
assert.equal(serialized.includes('api_key'), false);
assert.equal(serialized.includes('secret-token'), false);
assert.equal(serialized.includes('/Videos/item-1/stream'), false);
});
test('service deduplicates identical updates and sends changed timeline', async () => {
const sent: DiscordActivityPayload[] = [];
const timers = new Map<number, () => void>();
+13 -1
View File
@@ -106,6 +106,15 @@ function basename(filePath: string | null): string {
return parts[parts.length - 1] ?? '';
}
function fallbackTitleFromMediaPath(mediaPath: string | null): string {
const trimmed = mediaPath?.trim();
if (!trimmed) return '';
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) && !trimmed.toLowerCase().startsWith('file://')) {
return '';
}
return basename(trimmed).split(/[?#]/)[0] ?? '';
}
function buildStatus(snapshot: DiscordPresenceSnapshot): string {
if (!snapshot.connected || !snapshot.mediaPath) return 'Idle';
if (snapshot.paused) return 'Paused';
@@ -130,7 +139,10 @@ export function buildDiscordPresenceActivity(
): DiscordActivityPayload {
const style = resolvePresenceStyle(config.presenceStyle);
const status = buildStatus(snapshot);
const title = sanitizeText(snapshot.mediaTitle, basename(snapshot.mediaPath) || 'Unknown media');
const title = sanitizeText(
snapshot.mediaTitle,
fallbackTitleFromMediaPath(snapshot.mediaPath) || 'Unknown media',
);
const details =
snapshot.connected && snapshot.mediaPath ? trimField(title) : style.fallbackDetails;
const timeline = `${formatClock(snapshot.currentTimeSec)} / ${formatClock(snapshot.mediaDurationSec)}`;