From 1a84b348ad430008131369067a7807380774c3e3 Mon Sep 17 00:00:00 2001 From: lashman Date: Mon, 6 Apr 2026 04:33:21 +0300 Subject: [PATCH] fix four audit bugs - offline playback, audio passthrough, notification dedup, dashboard fixes --- src/hooks/use-jellyfin.ts | 4 +- src/hooks/use-new-releases.ts | 4 +- src/lib/device-profile.ts | 6 ++- src/pages/PlayerPage.tsx | 7 ++++ .../settings/sections/ServerDashboard.tsx | 38 ++----------------- 5 files changed, 19 insertions(+), 40 deletions(-) diff --git a/src/hooks/use-jellyfin.ts b/src/hooks/use-jellyfin.ts index dd2ceb3..04e1092 100644 --- a/src/hooks/use-jellyfin.ts +++ b/src/hooks/use-jellyfin.ts @@ -4,6 +4,7 @@ import { jellyfinClient, getItemsApi, getTvShowsApi, getUserViewsApi, getSearchA import { getLiveTvApi } from '@jellyfin/sdk/lib/utils/api/live-tv-api' import { debugLog } from '../lib/log' import { browserDeviceProfile } from '../lib/device-profile' +import { usePreferencesStore } from '../stores/preferences-store' function useApi() { return jellyfinClient.getApi() @@ -621,6 +622,7 @@ export function usePlaybackInfo( maxStreamingBitrate?: number, ) { const api = useApi() + const audioPassthrough = usePreferencesStore(s => s.audioPassthrough) return useQuery({ queryKey: [ 'jellyfin', @@ -644,7 +646,7 @@ export function usePlaybackInfo( // picks the default; when set, the returned TranscodingUrl muxes // that audio track into the stream. AudioStreamIndex: audioStreamIndex, - DeviceProfile: browserDeviceProfile() as any, + DeviceProfile: browserDeviceProfile(audioPassthrough) as any, AutoOpenLiveStream: true, EnableDirectPlay: true, EnableDirectStream: true, diff --git a/src/hooks/use-new-releases.ts b/src/hooks/use-new-releases.ts index 9eba15d..7fe843e 100644 --- a/src/hooks/use-new-releases.ts +++ b/src/hooks/use-new-releases.ts @@ -13,7 +13,7 @@ export function useNewReleaseNotifications(enabled: boolean) { const qc = useQueryClient() const lastNotifiedRef = useRef( (() => { - try { return localStorage.getItem('jf_last_notify_date') } catch { return null } + try { return localStorage.getItem('jf_bell_opened') } catch { return null } })() ) @@ -51,7 +51,7 @@ export function useNewReleaseNotifications(enabled: boolean) { const newest = items[0]?.DateCreated if (newest) { lastNotifiedRef.current = newest - try { localStorage.setItem('jf_last_notify_date', newest) } catch { /* noop */ } + try { localStorage.setItem('jf_bell_opened', newest) } catch { /* noop */ } } // Refresh the home page recently-added cache qc.invalidateQueries({ queryKey: ['jellyfin', 'home', 'recentlyAdded'] }) diff --git a/src/lib/device-profile.ts b/src/lib/device-profile.ts index 7606f68..5722f9b 100644 --- a/src/lib/device-profile.ts +++ b/src/lib/device-profile.ts @@ -75,7 +75,7 @@ function supportedVideoRanges(): string { return ranges.join('|') } -export function browserDeviceProfile() { +export function browserDeviceProfile(audioPassthrough = false) { const videoCodecs = supportedVideoCodecs() const videoCodecsCsv = videoCodecs.join(',') const videoRanges = supportedVideoRanges() @@ -91,7 +91,9 @@ export function browserDeviceProfile() { Container: 'mp4,m4v', Type: 'Video', VideoCodec: videoCodecsCsv, - AudioCodec: 'aac,mp3,ac3,eac3,flac,opus', + AudioCodec: audioPassthrough + ? 'aac,mp3,ac3,eac3,flac,opus,truehd,dts' + : 'aac,mp3,ac3,eac3,flac,opus', }, { Container: 'webm', diff --git a/src/pages/PlayerPage.tsx b/src/pages/PlayerPage.tsx index 7fa74a3..897c51e 100644 --- a/src/pages/PlayerPage.tsx +++ b/src/pages/PlayerPage.tsx @@ -291,6 +291,12 @@ export default function PlayerPage() { // native audioTracks switch isn't possible (transcoded streams, or // single-audio sources where the alternate track isn't in the file). // maxBitrate threads the user-picked quality cap into the same call. + // Offline fallback: if this item was downloaded, use the Blob URL + // instead of hitting the server. PlaybackInfo and reporting gracefully + // no-op when there's no connection. + const downloaded = useDownloads(s => s.getByItemId(id || '')) + const offlineUrl = downloaded?.status === 'done' ? downloaded.localPath : undefined + const { data: playbackInfo } = usePlaybackInfo( id, startTimeTicks, @@ -299,6 +305,7 @@ export default function PlayerPage() { ) const resolvedSource = playbackInfo?.MediaSources?.[0] const streamUrl = (() => { + if (offlineUrl) return offlineUrl if (!resolvedSource || !serverUrl) return '' // Direct play: server-confirmed the browser can decode the source as-is if (resolvedSource.SupportsDirectPlay) { diff --git a/src/pages/settings/sections/ServerDashboard.tsx b/src/pages/settings/sections/ServerDashboard.tsx index e68db43..ac17b83 100644 --- a/src/pages/settings/sections/ServerDashboard.tsx +++ b/src/pages/settings/sections/ServerDashboard.tsx @@ -1,8 +1,8 @@ import { useMemo } from 'react' import { useQuery } from '@tanstack/react-query' import { motion } from 'framer-motion' -import { Activity, MonitorPlay, Users, Server as ServerIcon, Clock, Film } from '../../../lib/icons' -import { jellyfinClient, getSystemApi, getSessionApi, getActivityLogApi } from '../../../api/jellyfin' +import { Activity, MonitorPlay, Users, Server as ServerIcon, Clock } from '../../../lib/icons' +import { jellyfinClient, getSystemApi, getSessionApi } from '../../../api/jellyfin' import { Section, SubHeading } from '../_ui' function useApi() { @@ -33,17 +33,6 @@ export function ServerDashboardSection() { refetchInterval: 10_000, }) - const activity = useQuery({ - queryKey: ['jellyfin', 'activity-log'], - queryFn: async () => { - if (!api) return [] - const res = await getActivityLogApi(api).getLogEntries({ limit: 20 }) - return res.data.Items || [] - }, - enabled: !!api, - refetchInterval: 60_000, - }) - const info = serverInfo.data const activeSessions = sessions.data?.filter(s => s.NowPlayingItem) || [] const transcodes = activeSessions.filter(s => s.PlayState?.PlayMethod === 'Transcode') @@ -107,28 +96,7 @@ export function ServerDashboardSection() { )} - {activity.data && activity.data.length > 0 && ( - <> - -
- {activity.data.slice(0, 10).map((entry: any, i: number) => ( - - - {entry.Name} - - {entry.DateCreated ? new Date(entry.DateCreated).toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' }) : ''} - - - ))} -
- - )} + ) }