type pages and utilities
This commit is contained in:
@@ -52,7 +52,7 @@ export function ensureAudioGraph(video: HTMLVideoElement): AudioGraph | null {
|
|||||||
if (graph) detachAudioGraph()
|
if (graph) detachAudioGraph()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const Ctor = window.AudioContext || (window as any).webkitAudioContext
|
const Ctor = window.AudioContext || window.webkitAudioContext
|
||||||
if (!Ctor) return null
|
if (!Ctor) return null
|
||||||
const ctx: AudioContext = new Ctor()
|
const ctx: AudioContext = new Ctor()
|
||||||
const source = ctx.createMediaElementSource(video)
|
const source = ctx.createMediaElementSource(video)
|
||||||
|
|||||||
+1
-1
@@ -54,7 +54,7 @@ export async function sendToSession(sessionId: string, itemId: string): Promise<
|
|||||||
if (!api) throw new Error('Not authenticated')
|
if (!api) throw new Error('Not authenticated')
|
||||||
await getSessionApi(api).play({
|
await getSessionApi(api).play({
|
||||||
sessionId,
|
sessionId,
|
||||||
playCommand: 'PlayNow' as any,
|
playCommand: 'PlayNow' as const,
|
||||||
itemIds: [itemId],
|
itemIds: [itemId],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export async function startDownload(args: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const blob = new Blob(chunks as any)
|
const blob = new Blob(chunks)
|
||||||
const objectUrl = URL.createObjectURL(blob)
|
const objectUrl = URL.createObjectURL(blob)
|
||||||
store.update(dl.id, {
|
store.update(dl.id, {
|
||||||
status: 'done',
|
status: 'done',
|
||||||
|
|||||||
@@ -43,9 +43,9 @@ export function deriveEpisodeMeta(
|
|||||||
// against the main ensemble happens at the consuming component.
|
// against the main ensemble happens at the consuming component.
|
||||||
const guests = (credits?.cast || []).slice(0, 8)
|
const guests = (credits?.cast || []).slice(0, 8)
|
||||||
const crew = credits?.crew || []
|
const crew = credits?.crew || []
|
||||||
const director = crew.find(c => (c as any).job === 'Director') || null
|
const director = crew.find(c => c.job === 'Director') || null
|
||||||
const writer = crew.find(
|
const writer = crew.find(
|
||||||
c => (c as any).job === 'Writer' || (c as any).job === 'Screenplay',
|
c => c.job === 'Writer' || c.job === 'Screenplay',
|
||||||
) || null
|
) || null
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Typed accessors for fields the Jellyfin SDK doesn't fully model and for
|
* Typed accessors for fields the Jellyfin SDK doesn't fully model and for
|
||||||
* synthetic fields the app stamps on `BaseItemDto` to bridge TMDB results
|
* synthetic fields the app stamps on `BaseItemDto` to bridge TMDB results
|
||||||
* into the `PosterCard` pipeline. Keeps the `as any` casts in one place
|
* into the `PosterCard` pipeline. Centralizes type-normalization for items entering PosterCard
|
||||||
* so call sites can read fields directly with confidence.
|
* so call sites can read fields directly with confidence.
|
||||||
*/
|
*/
|
||||||
import type { BaseItemDto } from '../api/types'
|
import type { BaseItemDto } from '../api/types'
|
||||||
|
|||||||
+1
-1
@@ -102,7 +102,7 @@ export function topByPersonRole(
|
|||||||
): PersonShare[] {
|
): PersonShare[] {
|
||||||
const buckets = new Map<string, number>()
|
const buckets = new Map<string, number>()
|
||||||
for (const it of items) {
|
for (const it of items) {
|
||||||
const people = ((it as any).People || []) as Array<{ Name?: string | null; Type?: string | null; Role?: string | null }>
|
const people = (it.People || []) as Array<{ Name?: string | null; Type?: string | null; Role?: string | null }>
|
||||||
const seen = new Set<string>()
|
const seen = new Set<string>()
|
||||||
for (const p of people) {
|
for (const p of people) {
|
||||||
if (!p.Name) continue
|
if (!p.Name) continue
|
||||||
|
|||||||
+2
-2
@@ -250,7 +250,7 @@ export async function addToTraktWatchlist(item: BaseItemDto): Promise<boolean> {
|
|||||||
if (!token) return false
|
if (!token) return false
|
||||||
const body = await buildScrobbleBody(item, 0)
|
const body = await buildScrobbleBody(item, 0)
|
||||||
if (!body) return false
|
if (!body) return false
|
||||||
const payload = item.Type === 'Movie' ? { movies: [{ ids: (body as any).movie.ids }] } : { shows: [{ ids: (body as any).show.ids }] }
|
const payload = item.Type === 'Movie' ? { movies: [{ ids: body.movie.ids }] } : { shows: [{ ids: body.show.ids }] }
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${BASE}/sync/watchlist`, {
|
const res = await fetch(`${BASE}/sync/watchlist`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -269,7 +269,7 @@ export async function removeFromTraktWatchlist(item: BaseItemDto): Promise<boole
|
|||||||
if (!token) return false
|
if (!token) return false
|
||||||
const body = await buildScrobbleBody(item, 0)
|
const body = await buildScrobbleBody(item, 0)
|
||||||
if (!body) return false
|
if (!body) return false
|
||||||
const payload = item.Type === 'Movie' ? { movies: [{ ids: (body as any).movie.ids }] } : { shows: [{ ids: (body as any).show.ids }] }
|
const payload = item.Type === 'Movie' ? { movies: [{ ids: body.movie.ids }] } : { shows: [{ ids: body.show.ids }] }
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${BASE}/sync/watchlist/remove`, {
|
const res = await fetch(`${BASE}/sync/watchlist/remove`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ function LibraryDetailPage({ id }: { id?: string }) {
|
|||||||
const sampleEpisode = useSampleEpisode(id, itemType === 'Series')
|
const sampleEpisode = useSampleEpisode(id, itemType === 'Series')
|
||||||
|
|
||||||
// Versions selector state
|
// Versions selector state
|
||||||
const sources = (item?.MediaSources || []) as any[]
|
const sources = item?.MediaSources || []
|
||||||
const [selectedSourceId, setSelectedSourceId] = useState<string | null>(null)
|
const [selectedSourceId, setSelectedSourceId] = useState<string | null>(null)
|
||||||
const activeSourceId = selectedSourceId || sources[0]?.Id || null
|
const activeSourceId = selectedSourceId || sources[0]?.Id || null
|
||||||
|
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ export default function HomePage() {
|
|||||||
ImageTags: {},
|
ImageTags: {},
|
||||||
ProviderIds: { Tmdb: String(r.id) },
|
ProviderIds: { Tmdb: String(r.id) },
|
||||||
_tmdbPoster: r.poster_path ? getTmdbImageUrl(r.poster_path, 'w342') : undefined,
|
_tmdbPoster: r.poster_path ? getTmdbImageUrl(r.poster_path, 'w342') : undefined,
|
||||||
} as any as BaseItemDto))}
|
} as BaseItemDto))}
|
||||||
/>
|
/>
|
||||||
</RowBoundary>
|
</RowBoundary>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -148,8 +148,8 @@ export default function LibraryPage({ type }: Props) {
|
|||||||
const items = useMemo(() => {
|
const items = useMemo(() => {
|
||||||
return allItems.filter(it => {
|
return allItems.filter(it => {
|
||||||
if (genreFilter && !(it.Genres || []).includes(genreFilter)) return false
|
if (genreFilter && !(it.Genres || []).includes(genreFilter)) return false
|
||||||
if (only4K && resolutionLabel(it as any) !== '4K') return false
|
if (only4K && resolutionLabel(it) !== '4K') return false
|
||||||
if (onlyHdr && !videoRangeLabel(it as any)) return false
|
if (onlyHdr && !videoRangeLabel(it)) return false
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
}, [allItems, genreFilter, only4K, onlyHdr])
|
}, [allItems, genreFilter, only4K, onlyHdr])
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ function ChannelsList() {
|
|||||||
return (
|
return (
|
||||||
<ul className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-2">
|
<ul className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-2">
|
||||||
{channels.map(ch => {
|
{channels.map(ch => {
|
||||||
const program = (ch as any).CurrentProgram as { Name?: string; StartDate?: string; EndDate?: string } | undefined
|
const program = ch.CurrentProgram as { Name?: string; StartDate?: string; EndDate?: string } | undefined
|
||||||
const imageTag = ch.ImageTags?.Primary
|
const imageTag = ch.ImageTags?.Primary
|
||||||
const img = ch.Id && imageTag ? getImageUrl(serverUrl, ch.Id, 'Primary', 128, imageTag) : ''
|
const img = ch.Id && imageTag ? getImageUrl(serverUrl, ch.Id, 'Primary', 128, imageTag) : ''
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export default function PersonPage() {
|
|||||||
if (filter === 'Acting') {
|
if (filter === 'Acting') {
|
||||||
merged = merged.filter(c => c._kind === 'cast')
|
merged = merged.filter(c => c._kind === 'cast')
|
||||||
} else {
|
} else {
|
||||||
merged = merged.filter(c => c._kind === 'crew' && (c as any).department === filter)
|
merged = merged.filter(c => c._kind === 'crew' && c.department === filter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,7 +260,7 @@ export default function PersonPage() {
|
|||||||
{title}
|
{title}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-[11px] text-text-3 truncate leading-tight mt-0.5">
|
<p className="text-[11px] text-text-3 truncate leading-tight mt-0.5">
|
||||||
{(c as any).character ? (c as any).character : (c as any).job}
|
{c.character ? c.character : c.job}
|
||||||
{year && (
|
{year && (
|
||||||
<>
|
<>
|
||||||
<span className="text-text-5 mx-1">·</span>
|
<span className="text-text-5 mx-1">·</span>
|
||||||
|
|||||||
+19
-19
@@ -247,7 +247,7 @@ export default function PlayerPage() {
|
|||||||
} = usePlayerChrome(isPaused)
|
} = usePlayerChrome(isPaused)
|
||||||
|
|
||||||
const subtitleStreams = item ? getSubtitleStreams(item) : []
|
const subtitleStreams = item ? getSubtitleStreams(item) : []
|
||||||
const mediaSourceId = ((item as any)?.MediaSources?.[0]?.Id as string | undefined) || undefined
|
const mediaSourceId = item?.MediaSources?.[0]?.Id || undefined
|
||||||
|
|
||||||
const {
|
const {
|
||||||
seriesId,
|
seriesId,
|
||||||
@@ -485,7 +485,7 @@ export default function PlayerPage() {
|
|||||||
// on every meaningful state tick until the native volume actually
|
// on every meaningful state tick until the native volume actually
|
||||||
// matches the pref - that's the only reliable signal.
|
// matches the pref - that's the only reliable signal.
|
||||||
function getNativeVideo(): HTMLVideoElement | null {
|
function getNativeVideo(): HTMLVideoElement | null {
|
||||||
const el = (player as any)?.el as HTMLElement | undefined
|
const el = (player as { el?: HTMLElement } | null)?.el
|
||||||
return (el?.querySelector('video') as HTMLVideoElement | null) || null
|
return (el?.querySelector('video') as HTMLVideoElement | null) || null
|
||||||
}
|
}
|
||||||
function applyVolume() {
|
function applyVolume() {
|
||||||
@@ -594,7 +594,7 @@ export default function PlayerPage() {
|
|||||||
|
|
||||||
// Fallback: chapters with intro/credits names
|
// Fallback: chapters with intro/credits names
|
||||||
if (out.length === 0) {
|
if (out.length === 0) {
|
||||||
const chapters = ((item as any)?.Chapters || []) as { Name?: string; StartPositionTicks?: number }[]
|
const chapters = (item?.Chapters || []) as { Name?: string; StartPositionTicks?: number }[]
|
||||||
if (chapters.length && duration) {
|
if (chapters.length && duration) {
|
||||||
for (let i = 0; i < chapters.length; i++) {
|
for (let i = 0; i < chapters.length; i++) {
|
||||||
const c = chapters[i]
|
const c = chapters[i]
|
||||||
@@ -670,7 +670,7 @@ export default function PlayerPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. Browser's native list on the underlying <video>
|
// 2. Browser's native list on the underlying <video>
|
||||||
const el = (player as any).el as HTMLElement | undefined
|
const el = (player as { el?: HTMLElement } | null)?.el
|
||||||
const video = el?.querySelector?.('video') as HTMLVideoElement | null
|
const video = el?.querySelector?.('video') as HTMLVideoElement | null
|
||||||
if (video?.textTracks) {
|
if (video?.textTracks) {
|
||||||
for (let i = 0; i < video.textTracks.length; i++) {
|
for (let i = 0; i < video.textTracks.length; i++) {
|
||||||
@@ -749,13 +749,13 @@ export default function PlayerPage() {
|
|||||||
|
|
||||||
/* ── Helpers: PiP, screenshot, theater, frame step, etc. ─── */
|
/* ── Helpers: PiP, screenshot, theater, frame step, etc. ─── */
|
||||||
function togglePictureInPicture() {
|
function togglePictureInPicture() {
|
||||||
const el = (playerRef.current as any)?.el as HTMLElement | undefined
|
const el = (playerRef.current as { el?: HTMLElement } | null)?.el
|
||||||
const video = el?.querySelector('video') as HTMLVideoElement | null
|
const video = el?.querySelector('video') as HTMLVideoElement | null
|
||||||
if (!video) return
|
if (!video) return
|
||||||
if ((document as any).pictureInPictureElement === video) {
|
if (document.pictureInPictureElement === video) {
|
||||||
;(document as any).exitPictureInPicture?.().catch(() => {})
|
;document.exitPictureInPicture?.().catch(() => {})
|
||||||
} else {
|
} else {
|
||||||
;(video as any).requestPictureInPicture?.().catch(() => {})
|
;video.requestPictureInPicture?.().catch(() => {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -764,7 +764,7 @@ export default function PlayerPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function takeScreenshot() {
|
function takeScreenshot() {
|
||||||
const el = (playerRef.current as any)?.el as HTMLElement | undefined
|
const el = (playerRef.current as { el?: HTMLElement } | null)?.el
|
||||||
const video = el?.querySelector('video') as HTMLVideoElement | null
|
const video = el?.querySelector('video') as HTMLVideoElement | null
|
||||||
if (!video) return
|
if (!video) return
|
||||||
try {
|
try {
|
||||||
@@ -789,7 +789,7 @@ export default function PlayerPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getFps(): number {
|
function getFps(): number {
|
||||||
const streams = ((item as any)?.MediaSources?.[0]?.MediaStreams || []) as any[]
|
const streams = item?.MediaSources?.[0]?.MediaStreams || []
|
||||||
const v = streams.find(s => s.Type === 'Video')
|
const v = streams.find(s => s.Type === 'Video')
|
||||||
const fps = Number(v?.RealFrameRate ?? v?.AverageFrameRate ?? 0)
|
const fps = Number(v?.RealFrameRate ?? v?.AverageFrameRate ?? 0)
|
||||||
return Number.isFinite(fps) && fps > 1 ? fps : 24
|
return Number.isFinite(fps) && fps > 1 ? fps : 24
|
||||||
@@ -819,13 +819,13 @@ export default function PlayerPage() {
|
|||||||
try {
|
try {
|
||||||
p.playbackRate = rate
|
p.playbackRate = rate
|
||||||
} catch { /* not ready */ }
|
} catch { /* not ready */ }
|
||||||
const el = (p as any)?.el as HTMLElement | undefined
|
const el = (p as { el?: HTMLElement } | null)?.el
|
||||||
const video = el?.querySelector('video') as HTMLVideoElement | null
|
const video = el?.querySelector('video') as HTMLVideoElement | null
|
||||||
if (video) {
|
if (video) {
|
||||||
video.playbackRate = rate
|
video.playbackRate = rate
|
||||||
;(video as any).preservesPitch = preserveAudioPitch
|
;video.preservesPitch = preserveAudioPitch
|
||||||
;(video as any).mozPreservesPitch = preserveAudioPitch
|
;video.mozPreservesPitch = preserveAudioPitch
|
||||||
;(video as any).webkitPreservesPitch = preserveAudioPitch
|
;video.webkitPreservesPitch = preserveAudioPitch
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -970,7 +970,7 @@ export default function PlayerPage() {
|
|||||||
|
|
||||||
/* Track lists from Jellyfin metadata */
|
/* Track lists from Jellyfin metadata */
|
||||||
const audioTracks = item ? getAudioStreams(item) : []
|
const audioTracks = item ? getAudioStreams(item) : []
|
||||||
const videoStream = ((item as any)?.MediaSources?.[0]?.MediaStreams || []).find((s: any) => s.Type === 'Video')
|
const videoStream = (item?.MediaSources?.[0]?.MediaStreams || []).find((s: { Type?: string }) => s.Type === 'Video')
|
||||||
const videoWidth = videoStream?.Width || 1920
|
const videoWidth = videoStream?.Width || 1920
|
||||||
const videoHeight = videoStream?.Height || 1080
|
const videoHeight = videoStream?.Height || 1080
|
||||||
|
|
||||||
@@ -985,9 +985,9 @@ export default function PlayerPage() {
|
|||||||
setAudioIndex(jfIndex)
|
setAudioIndex(jfIndex)
|
||||||
if (jfIndex == null) return
|
if (jfIndex == null) return
|
||||||
if (resolvedSource?.SupportsDirectPlay) {
|
if (resolvedSource?.SupportsDirectPlay) {
|
||||||
const el = (playerRef.current as any)?.el as HTMLElement | undefined
|
const el = (playerRef.current as { el?: HTMLElement } | null)?.el
|
||||||
const video = el?.querySelector('video') as HTMLVideoElement | null
|
const video = el?.querySelector('video') as HTMLVideoElement | null
|
||||||
const native = (video as any)?.audioTracks as { length: number; [i: number]: any } | undefined
|
const native = video.audioTracks as { length: number; [i: number]: { enabled: boolean } } | undefined
|
||||||
if (native && native.length > 1) {
|
if (native && native.length > 1) {
|
||||||
const target = audioTracks.find(t => t.Index === jfIndex)
|
const target = audioTracks.find(t => t.Index === jfIndex)
|
||||||
const targetLang = (target?.Language || '').toLowerCase()
|
const targetLang = (target?.Language || '').toLowerCase()
|
||||||
@@ -1005,7 +1005,7 @@ export default function PlayerPage() {
|
|||||||
setStreamAudioIndex(jfIndex)
|
setStreamAudioIndex(jfIndex)
|
||||||
}
|
}
|
||||||
const subtitleTracks = item ? getSubtitleStreams(item) : []
|
const subtitleTracks = item ? getSubtitleStreams(item) : []
|
||||||
const chapters = (item?.Chapters || []) as any[]
|
const chapters = item?.Chapters || []
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1200,7 +1200,7 @@ export default function PlayerPage() {
|
|||||||
onClose={() => setEpisodesOpen(false)}
|
onClose={() => setEpisodesOpen(false)}
|
||||||
seriesId={seriesId}
|
seriesId={seriesId}
|
||||||
currentItemId={id}
|
currentItemId={id}
|
||||||
initialSeasonId={(item as any)?.SeasonId || undefined}
|
initialSeasonId={item?.SeasonId || undefined}
|
||||||
serverUrl={serverUrl}
|
serverUrl={serverUrl}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -121,16 +121,16 @@ export default function SearchPage() {
|
|||||||
const localSeries = (grouped.Series || []) as BaseItemDto[]
|
const localSeries = (grouped.Series || []) as BaseItemDto[]
|
||||||
const localTmdbIds = new Set<string>()
|
const localTmdbIds = new Set<string>()
|
||||||
for (const it of [...localMovies, ...localSeries]) {
|
for (const it of [...localMovies, ...localSeries]) {
|
||||||
const t = (it as any).ProviderIds?.Tmdb
|
const t = it.ProviderIds?.Tmdb
|
||||||
if (t) localTmdbIds.add(String(t))
|
if (t) localTmdbIds.add(String(t))
|
||||||
}
|
}
|
||||||
const tmdbExtras = [...tmdbMappedMovies, ...tmdbMappedTv].filter(it => {
|
const tmdbExtras = [...tmdbMappedMovies, ...tmdbMappedTv].filter(it => {
|
||||||
const t = (it as any).ProviderIds?.Tmdb
|
const t = it.ProviderIds?.Tmdb
|
||||||
return !!t && !localTmdbIds.has(String(t))
|
return !!t && !localTmdbIds.has(String(t))
|
||||||
})
|
})
|
||||||
|
|
||||||
const localCards: BaseItemDto[] = [...localMovies, ...localSeries].map(
|
const localCards: BaseItemDto[] = [...localMovies, ...localSeries].map(
|
||||||
it => ({ ...(it as any), _inLibrary: true } as BaseItemDto),
|
it => ({ ...it, _inLibrary: true } as BaseItemDto),
|
||||||
)
|
)
|
||||||
const mixedCards: BaseItemDto[] = [...localCards, ...tmdbExtras]
|
const mixedCards: BaseItemDto[] = [...localCards, ...tmdbExtras]
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export default function TmdbDetailPage({ tmdbId, kind }: Props) {
|
|||||||
// PNG wordmarks) → TMDB logos. Mirrors the library DetailHero's chain
|
// PNG wordmarks) → TMDB logos. Mirrors the library DetailHero's chain
|
||||||
// (jellyfin → fanart) but without the jellyfin source since this page
|
// (jellyfin → fanart) but without the jellyfin source since this page
|
||||||
// exists precisely for items not in the library.
|
// exists precisely for items not in the library.
|
||||||
const tvdbId = (enrichment.data as any)?.external_ids?.tvdb_id
|
const tvdbId = enrichment.data?.external_ids?.tvdb_id
|
||||||
const fanartMovieQuery = useFanartMovie(kind === 'movie' ? String(tmdbId) : null)
|
const fanartMovieQuery = useFanartMovie(kind === 'movie' ? String(tmdbId) : null)
|
||||||
const fanartTvQuery = useFanartTv(kind === 'tv' && tvdbId ? String(tvdbId) : null)
|
const fanartTvQuery = useFanartTv(kind === 'tv' && tvdbId ? String(tvdbId) : null)
|
||||||
const fanartLogo = pickBestFanartImage(
|
const fanartLogo = pickBestFanartImage(
|
||||||
@@ -135,7 +135,7 @@ export default function TmdbDetailPage({ tmdbId, kind }: Props) {
|
|||||||
// playback URL; otherwise the sticky bar's Play button has nothing
|
// playback URL; otherwise the sticky bar's Play button has nothing
|
||||||
// to do, so we hide it by passing an empty url and a no-op resume.
|
// to do, so we hide it by passing an empty url and a no-op resume.
|
||||||
const playUrl = matchedLocal ? `/play/${matchedLocal.id}` : ''
|
const playUrl = matchedLocal ? `/play/${matchedLocal.id}` : ''
|
||||||
const tmdbImdbId = (data as any).external_ids?.imdb_id || null
|
const tmdbImdbId = data.external_ids?.imdb_id || null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pb-12">
|
<div className="pb-12">
|
||||||
@@ -262,12 +262,12 @@ export default function TmdbDetailPage({ tmdbId, kind }: Props) {
|
|||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{kind === 'tv' && (data as any).number_of_seasons > 0 && (
|
{kind === 'tv' && data.number_of_seasons > 0 && (
|
||||||
<>
|
<>
|
||||||
<Dot />
|
<Dot />
|
||||||
<span className="inline-flex items-center gap-1 tabular-nums">
|
<span className="inline-flex items-center gap-1 tabular-nums">
|
||||||
<Calendar size={11} stroke={2} />
|
<Calendar size={11} stroke={2} />
|
||||||
{(data as any).number_of_seasons} season{(data as any).number_of_seasons === 1 ? '' : 's'}
|
{data.number_of_seasons} season{data.number_of_seasons === 1 ? '' : 's'}
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -287,7 +287,7 @@ export default function TmdbDetailPage({ tmdbId, kind }: Props) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center gap-2.5 flex-wrap">
|
<div className="flex items-center gap-2.5 flex-wrap">
|
||||||
<RequestButton tmdbId={tmdbId} kind={kind} tmdbData={data as any} />
|
<RequestButton tmdbId={tmdbId} kind={kind} tmdbData={data} />
|
||||||
{matchedLocal && (
|
{matchedLocal && (
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate(`/item/${matchedLocal.id}`)}
|
onClick={() => navigate(`/item/${matchedLocal.id}`)}
|
||||||
@@ -352,9 +352,9 @@ export default function TmdbDetailPage({ tmdbId, kind }: Props) {
|
|||||||
</Section>
|
</Section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{((data as any).videos?.results?.length ?? 0) > 0 && (
|
{(data.videos?.results?.length ?? 0) > 0 && (
|
||||||
<Section label="Videos">
|
<Section label="Videos">
|
||||||
<VideosSection videos={(data as any).videos?.results} />
|
<VideosSection videos={data.videos?.results} />
|
||||||
</Section>
|
</Section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -376,10 +376,10 @@ export default function TmdbDetailPage({ tmdbId, kind }: Props) {
|
|||||||
</Section>
|
</Section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{kind === 'movie' && (data as any).belongs_to_collection && (
|
{kind === 'movie' && data.belongs_to_collection && (
|
||||||
<Section label="Collection">
|
<Section label="Collection">
|
||||||
<CollectionStrip
|
<CollectionStrip
|
||||||
collectionRef={(data as any).belongs_to_collection}
|
collectionRef={data.belongs_to_collection}
|
||||||
collection={tmdbCollection.data}
|
collection={tmdbCollection.data}
|
||||||
currentMovieId={tmdbId}
|
currentMovieId={tmdbId}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ export function WatchlistRow() {
|
|||||||
<ContentRow
|
<ContentRow
|
||||||
title="Your watchlist"
|
title="Your watchlist"
|
||||||
subtitle="Saved for later"
|
subtitle="Saved for later"
|
||||||
items={items as any}
|
items={items}
|
||||||
seeAllHref="/playlists"
|
seeAllHref="/playlists"
|
||||||
layoutKey="watchlist"
|
layoutKey="watchlist"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export function ServerDashboardSection() {
|
|||||||
const transcodes = activeSessions.filter(s => s.PlayState?.PlayMethod === 'Transcode')
|
const transcodes = activeSessions.filter(s => s.PlayState?.PlayMethod === 'Transcode')
|
||||||
|
|
||||||
const uptime = useMemo(() => {
|
const uptime = useMemo(() => {
|
||||||
const start = (info as any)?.ServerStartTime
|
const start = info?.ServerStartTime
|
||||||
if (!start) return null
|
if (!start) return null
|
||||||
const ms = Date.now() - new Date(start).getTime()
|
const ms = Date.now() - new Date(start).getTime()
|
||||||
const days = Math.floor(ms / 86_400_000)
|
const days = Math.floor(ms / 86_400_000)
|
||||||
|
|||||||
Reference in New Issue
Block a user