bug处理

This commit is contained in:
truewhile
2026-09-14 21:16:34 +08:00
parent b28c0d9247
commit 1ec8303fec
9 changed files with 677 additions and 80 deletions
+5 -1
View File
@@ -124,6 +124,7 @@ export function LibraryPage() {
})
const {
resolvingSeries,
selectedEpisodes,
visibleEpisodes,
selectedSeriesEpisodes,
@@ -132,6 +133,7 @@ export function LibraryPage() {
handleSeriesClick,
clearSelectedSeries,
} = useLibrarySeriesSelection({
libraryID: id,
items,
seriesEpisodeItems,
isSeriesLibrary,
@@ -209,7 +211,9 @@ export function LibraryPage() {
return <>{actions}</>
}, [canFavorite, favouriteBusyID, handleToggleFavourite, isFavourite, movieActions])
if (loading) {
// resolvingSeries:深链目标剧集不在已加载的一页卡片里,正在按 key
// 单独解析。此时先显示加载态,避免先渲染整个媒体库列表再跳走。
if (loading || resolvingSeries) {
return (
<div className="flex items-center justify-center py-32">
<motion.div animate={{ opacity: [0.4, 1, 0.4] }} transition={{ repeat: Infinity, duration: 2 }} className="flex items-center gap-3">
+51 -6
View File
@@ -1,4 +1,4 @@
import { useEffect, useMemo } from 'react'
import { useEffect, useMemo, useState } from 'react'
import type { Media } from '../types'
import {
@@ -9,6 +9,7 @@ import {
specialSectionForMedia,
type SeriesCard,
} from '../utils/groupSeries'
import { resolveSeriesCardByKey } from '../utils/seriesCardResolve'
type SeasonEpisodes = {
season: number
@@ -16,6 +17,7 @@ type SeasonEpisodes = {
}
type UseLibrarySeriesSelectionOptions = {
libraryID: string
items: Media[]
seriesEpisodeItems: Media[]
isSeriesLibrary: boolean
@@ -32,6 +34,7 @@ type UseLibrarySeriesSelectionOptions = {
}
export function useLibrarySeriesSelection({
libraryID,
items,
seriesEpisodeItems,
isSeriesLibrary,
@@ -46,6 +49,14 @@ export function useLibrarySeriesSelection({
setSelectedSeason,
onClearSeriesState,
}: UseLibrarySeriesSelectionOptions) {
// 深链(首页最新条目 / 海报墙 ?series=<key>)指向的剧集不在已加载的
// 一页卡片里时,需要单独按 key 解析;resolvingSeries 让页面在这段时间
// 显示加载态,而不是先闪一下整个媒体库列表。
const [resolvingSeries, setResolvingSeries] = useState(false)
const [pendingSeriesKey, setPendingSeriesKey] = useState('')
// 当前 URL 请求的剧集 key(非剧集库 / 加载中 / 没带参数时为空)。
const requestedSeriesKey = !loading && isSeries ? (searchParams.get('series') ?? '') : ''
const selectedEpisodes = useMemo(() => {
const sourceItems = isSeriesLibrary ? seriesEpisodeItems : items
if (!selectedSeries || sourceItems.length === 0) return []
@@ -92,20 +103,53 @@ export function useLibrarySeriesSelection({
useEffect(() => {
if (loading) return
if (!isSeries) {
setPendingSeriesKey('')
setSelectedSeries(null)
setSelectedSeason(null)
return
}
const key = searchParams.get('series')
if (!key) {
if (!requestedSeriesKey) {
setPendingSeriesKey('')
setSelectedSeries(null)
return
}
const next = seriesCards.find((card) => card.key === key)
setSelectedSeries(next ?? null)
}, [isSeries, loading, searchParams, seriesCards, setSelectedSeason, setSelectedSeries])
const next = seriesCards.find((card) => card.key === requestedSeriesKey)
if (next) {
setPendingSeriesKey('')
setSelectedSeries(next)
return
}
// 目标剧集尚未加载到(远程库分页 / 大库分页):登记待解析 key。
setPendingSeriesKey((prev) => (prev === requestedSeriesKey ? prev : requestedSeriesKey))
}, [isSeries, loading, requestedSeriesKey, seriesCards, setSelectedSeason, setSelectedSeries])
useEffect(() => {
// pendingSeriesKey 必须仍然是 URL 上请求的 key:切换媒体库/清空参数
// 时直接丢弃待解析状态,避免把别的库的剧集解析进来。
if (!pendingSeriesKey || pendingSeriesKey !== requestedSeriesKey) {
setResolvingSeries(false)
return
}
let cancelled = false
setResolvingSeries(true)
resolveSeriesCardByKey(libraryID, pendingSeriesKey)
.then((card) => {
if (cancelled) return
setResolvingSeries(false)
setSelectedSeries(card)
})
.catch(() => {
if (cancelled) return
setResolvingSeries(false)
setSelectedSeries(null)
})
return () => {
cancelled = true
}
}, [libraryID, pendingSeriesKey, requestedSeriesKey, setSelectedSeries])
useEffect(() => {
if (!selectedSeries || selectedEpisodes.length === 0) {
@@ -135,6 +179,7 @@ export function useLibrarySeriesSelection({
}
return {
resolvingSeries,
selectedEpisodes,
visibleEpisodes,
selectedSeriesEpisodes,
+2
View File
@@ -36,6 +36,7 @@ export type SeriesCard = {
linkMedia?: Media
linkLibraryId?: string
count: number
is_series?: boolean
last_added_at?: string
}
@@ -223,6 +224,7 @@ export function seasonLabel(season: number): string {
export function isSeriesCard(card: SeriesCard): boolean {
const linkMedia = card.linkMedia ?? card.rep
return (
card.is_series === true ||
card.count > 1 ||
isEpisodeLike(card.rep) ||
isEpisodeLike(linkMedia) ||
+59
View File
@@ -0,0 +1,59 @@
import { libraryAPI, mediaAPI } from '../api/library'
import type { Media } from '../types'
import { groupSeries, type SeriesCard } from './groupSeries'
import { isRemoteEmbyID } from './remoteEmby'
/**
* 按 series key 反解出剧集卡片。
*
* 首页「最新条目」、海报墙等入口点击后带 ?series=<key> 深链进媒体库页,
* 但媒体库页每次只加载一页剧集卡片(远程 Emby 库 100 条),目标剧集不在
* 这一页时列表里找不到对应卡片,页面就退化成整个媒体库列表。这里按 key
* 单独解析目标剧集,与分页无关:
*
* - 远程 Emby 的 key 本身就是(伪装后的)剧集 ID,直接取该条目详情;
* - 本地 key 是分组 hash,改取该剧集的集列表,再折叠出代表条目。
*
* key 失效(剧集已删除等)时返回 null,调用方保持"整库列表"原状态。
*/
export async function resolveSeriesCardByKey(libraryID: string, key: string): Promise<SeriesCard | null> {
if (!libraryID || !key) return null
if (isRemoteEmbyID(key)) {
const rep = await fetchRemoteSeriesMedia(key)
if (rep) {
return { key, rep, linkMedia: rep, count: 0, is_series: true }
}
}
const episodes = await fetchSeriesEpisodes(libraryID, key)
if (episodes.length === 0) return null
// 与列表页同一套折叠规则,选出海报/元数据最好的那一集做代表卡片。
const [grouped] = groupSeries(episodes)
const rep = grouped?.rep ?? episodes[0]
return {
key,
rep,
linkMedia: grouped?.linkMedia ?? rep,
count: episodes.length,
is_series: true,
}
}
async function fetchRemoteSeriesMedia(id: string): Promise<Media | null> {
try {
const media = await mediaAPI.get(id)
return media?.id ? media : null
} catch {
return null
}
}
async function fetchSeriesEpisodes(libraryID: string, key: string): Promise<Media[]> {
try {
const page = await libraryAPI.listSeriesEpisodes(libraryID, key)
return Array.isArray(page.items) ? page.items : []
} catch {
return []
}
}