fix four audit bugs - offline playback, audio passthrough, notification dedup, dashboard fixes
This commit is contained in:
@@ -4,6 +4,7 @@ import { jellyfinClient, getItemsApi, getTvShowsApi, getUserViewsApi, getSearchA
|
|||||||
import { getLiveTvApi } from '@jellyfin/sdk/lib/utils/api/live-tv-api'
|
import { getLiveTvApi } from '@jellyfin/sdk/lib/utils/api/live-tv-api'
|
||||||
import { debugLog } from '../lib/log'
|
import { debugLog } from '../lib/log'
|
||||||
import { browserDeviceProfile } from '../lib/device-profile'
|
import { browserDeviceProfile } from '../lib/device-profile'
|
||||||
|
import { usePreferencesStore } from '../stores/preferences-store'
|
||||||
|
|
||||||
function useApi() {
|
function useApi() {
|
||||||
return jellyfinClient.getApi()
|
return jellyfinClient.getApi()
|
||||||
@@ -621,6 +622,7 @@ export function usePlaybackInfo(
|
|||||||
maxStreamingBitrate?: number,
|
maxStreamingBitrate?: number,
|
||||||
) {
|
) {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
const audioPassthrough = usePreferencesStore(s => s.audioPassthrough)
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: [
|
queryKey: [
|
||||||
'jellyfin',
|
'jellyfin',
|
||||||
@@ -644,7 +646,7 @@ export function usePlaybackInfo(
|
|||||||
// picks the default; when set, the returned TranscodingUrl muxes
|
// picks the default; when set, the returned TranscodingUrl muxes
|
||||||
// that audio track into the stream.
|
// that audio track into the stream.
|
||||||
AudioStreamIndex: audioStreamIndex,
|
AudioStreamIndex: audioStreamIndex,
|
||||||
DeviceProfile: browserDeviceProfile() as any,
|
DeviceProfile: browserDeviceProfile(audioPassthrough) as any,
|
||||||
AutoOpenLiveStream: true,
|
AutoOpenLiveStream: true,
|
||||||
EnableDirectPlay: true,
|
EnableDirectPlay: true,
|
||||||
EnableDirectStream: true,
|
EnableDirectStream: true,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export function useNewReleaseNotifications(enabled: boolean) {
|
|||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
const lastNotifiedRef = useRef<string | null>(
|
const lastNotifiedRef = useRef<string | null>(
|
||||||
(() => {
|
(() => {
|
||||||
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
|
const newest = items[0]?.DateCreated
|
||||||
if (newest) {
|
if (newest) {
|
||||||
lastNotifiedRef.current = 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
|
// Refresh the home page recently-added cache
|
||||||
qc.invalidateQueries({ queryKey: ['jellyfin', 'home', 'recentlyAdded'] })
|
qc.invalidateQueries({ queryKey: ['jellyfin', 'home', 'recentlyAdded'] })
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ function supportedVideoRanges(): string {
|
|||||||
return ranges.join('|')
|
return ranges.join('|')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function browserDeviceProfile() {
|
export function browserDeviceProfile(audioPassthrough = false) {
|
||||||
const videoCodecs = supportedVideoCodecs()
|
const videoCodecs = supportedVideoCodecs()
|
||||||
const videoCodecsCsv = videoCodecs.join(',')
|
const videoCodecsCsv = videoCodecs.join(',')
|
||||||
const videoRanges = supportedVideoRanges()
|
const videoRanges = supportedVideoRanges()
|
||||||
@@ -91,7 +91,9 @@ export function browserDeviceProfile() {
|
|||||||
Container: 'mp4,m4v',
|
Container: 'mp4,m4v',
|
||||||
Type: 'Video',
|
Type: 'Video',
|
||||||
VideoCodec: videoCodecsCsv,
|
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',
|
Container: 'webm',
|
||||||
|
|||||||
@@ -291,6 +291,12 @@ export default function PlayerPage() {
|
|||||||
// native audioTracks switch isn't possible (transcoded streams, or
|
// native audioTracks switch isn't possible (transcoded streams, or
|
||||||
// single-audio sources where the alternate track isn't in the file).
|
// single-audio sources where the alternate track isn't in the file).
|
||||||
// maxBitrate threads the user-picked quality cap into the same call.
|
// 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(
|
const { data: playbackInfo } = usePlaybackInfo(
|
||||||
id,
|
id,
|
||||||
startTimeTicks,
|
startTimeTicks,
|
||||||
@@ -299,6 +305,7 @@ export default function PlayerPage() {
|
|||||||
)
|
)
|
||||||
const resolvedSource = playbackInfo?.MediaSources?.[0]
|
const resolvedSource = playbackInfo?.MediaSources?.[0]
|
||||||
const streamUrl = (() => {
|
const streamUrl = (() => {
|
||||||
|
if (offlineUrl) return offlineUrl
|
||||||
if (!resolvedSource || !serverUrl) return ''
|
if (!resolvedSource || !serverUrl) return ''
|
||||||
// Direct play: server-confirmed the browser can decode the source as-is
|
// Direct play: server-confirmed the browser can decode the source as-is
|
||||||
if (resolvedSource.SupportsDirectPlay) {
|
if (resolvedSource.SupportsDirectPlay) {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { motion } from 'framer-motion'
|
import { motion } from 'framer-motion'
|
||||||
import { Activity, MonitorPlay, Users, Server as ServerIcon, Clock, Film } from '../../../lib/icons'
|
import { Activity, MonitorPlay, Users, Server as ServerIcon, Clock } from '../../../lib/icons'
|
||||||
import { jellyfinClient, getSystemApi, getSessionApi, getActivityLogApi } from '../../../api/jellyfin'
|
import { jellyfinClient, getSystemApi, getSessionApi } from '../../../api/jellyfin'
|
||||||
import { Section, SubHeading } from '../_ui'
|
import { Section, SubHeading } from '../_ui'
|
||||||
|
|
||||||
function useApi() {
|
function useApi() {
|
||||||
@@ -33,17 +33,6 @@ export function ServerDashboardSection() {
|
|||||||
refetchInterval: 10_000,
|
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 info = serverInfo.data
|
||||||
const activeSessions = sessions.data?.filter(s => s.NowPlayingItem) || []
|
const activeSessions = sessions.data?.filter(s => s.NowPlayingItem) || []
|
||||||
const transcodes = activeSessions.filter(s => s.PlayState?.PlayMethod === 'Transcode')
|
const transcodes = activeSessions.filter(s => s.PlayState?.PlayMethod === 'Transcode')
|
||||||
@@ -107,28 +96,7 @@ export function ServerDashboardSection() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activity.data && activity.data.length > 0 && (
|
|
||||||
<>
|
|
||||||
<SubHeading label="Recent activity" />
|
|
||||||
<div className="space-y-1">
|
|
||||||
{activity.data.slice(0, 10).map((entry: any, i: number) => (
|
|
||||||
<motion.div
|
|
||||||
key={entry.Id || i}
|
|
||||||
initial={{ opacity: 0, x: -6 }}
|
|
||||||
animate={{ opacity: 1, x: 0 }}
|
|
||||||
transition={{ delay: Math.min(i * 0.03, 0.3) }}
|
|
||||||
className="flex items-center gap-3 px-2 py-1.5 text-[11.5px]"
|
|
||||||
>
|
|
||||||
<Film size={11} className="text-text-4 shrink-0" />
|
|
||||||
<span className="text-text-3 truncate flex-1">{entry.Name}</span>
|
|
||||||
<span className="text-text-4 tabular-nums shrink-0">
|
|
||||||
{entry.DateCreated ? new Date(entry.DateCreated).toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' }) : ''}
|
|
||||||
</span>
|
|
||||||
</motion.div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Section>
|
</Section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user