优化,bug处理

This commit is contained in:
truewhile
2026-10-12 23:43:40 +08:00
parent 6864a9c7f1
commit 23937d8137
9 changed files with 651 additions and 2 deletions
+12 -1
View File
@@ -20,7 +20,7 @@ import { useAuthStore } from '../stores/auth'
import type { Media, PlaybackInfo, PlaybackQuality, PlaybackSegment, PlaybackSegmentKind } from '../types'
import { getSeriesKey, seriesTitleFromPath } from '../utils/groupSeries'
import { formatEpisodeLabel } from '../utils/episodeNumber'
import { mediaVersionMatches, mediaVersionsOf } from '../utils/mediaVersion'
import { autoPlayEpisodeFor, mediaVersionMatches, mediaVersionsOf } from '../utils/mediaVersion'
import { DEFAULT_PLAYBACK_RATE, normalizePlaybackRate } from '../utils/playbackRate'
import { resolveActiveSkip, skippedNoticeText, toSkipSegments, type SkipPrompt } from '../utils/skipSegments'
import { isRemoteEmbyID } from '../utils/remoteEmby'
@@ -1681,6 +1681,17 @@ export function PlayerPage() {
return playlistEpisodes.findIndex((e) => mediaVersionMatches(e, media.id))
}, [media, playlistEpisodes])
// 用剧集 ID 打开播放器时(首页轮播、剧集库卡片、剧集详情页的播放按钮),先落到第一集:
// 这类条目没有可播文件,直接请求 /api/stream/<剧集ID> 会被远程 Emby 以 500 打回。
useEffect(() => {
const target = autoPlayEpisodeFor(media, id, playlistEpisodes)
if (!target) return
navigate(
{ pathname: `/play/${target.id}`, search: location.search },
{ replace: true, state: location.state },
)
}, [media, id, playlistEpisodes, navigate, location.search, location.state])
const prevEpisode = useMemo(() => {
if (currentEpisodeIndex > 0) {
return playlistEpisodes[currentEpisodeIndex - 1]
+22
View File
@@ -1,4 +1,5 @@
import {
autoPlayEpisodeFor,
isStrmMedia,
mediaVersionFileName,
mediaVersionLabel,
@@ -64,4 +65,25 @@ check('strm filename drops outer extension', mediaVersionFileName(cloudRow) ===
check('missing path has no filename', mediaVersionFileName(media({ path: '' })) === '')
check('isStrmMedia detects strm url', isStrmMedia(cloudRow))
// 首页轮播/剧集卡片交给播放器的是「整部剧」ID:选集列表里没有它自己,
// 播放器要自动落到第一集,否则 /api/stream/<剧集ID> 会被远程 Emby 打回 500。
const series = media({ id: 'embyremote~mount-1~222679', title: '某部新番', path: '/anime/某部新番', size_bytes: 0 })
const ep1 = media({ id: 'embyremote~mount-1~222681', path: '/anime/某部新番/Season 1/S1E01.mp4', size_bytes: 299580311 })
const ep2 = media({ id: 'embyremote~mount-1~222683', path: '/anime/某部新番/Season 1/S1E02.mp4', size_bytes: 299580312 })
check('series falls back to its first episode', autoPlayEpisodeFor(series, series.id, [ep1, ep2])?.id === ep1.id)
check('playable episode stays untouched', autoPlayEpisodeFor(ep1, ep1.id, [ep1, ep2]) === null)
check('single-item movie list stays untouched', autoPlayEpisodeFor(ep1, ep1.id, [{ ...ep1 }]) === null)
check('waits until the requested item is loaded', autoPlayEpisodeFor(null, series.id, [ep1]) === null)
check('ignores a response for another id', autoPlayEpisodeFor(ep2, series.id, [ep1]) === null)
check('empty episode list has no fallback', autoPlayEpisodeFor(series, series.id, []) === null)
check(
'entry present as another version stays untouched',
autoPlayEpisodeFor(
media({ id: 'v-4k' }),
'v-4k',
[media({ id: 'v-4k', versions: [local4k, local1080] })],
) === null,
)
console.log('mediaVersion.test.ts: ok')
+26
View File
@@ -195,6 +195,32 @@ export function mediaVersionSourceLabel(media: Media): string {
return isStrmMedia(media) ? '云端直链' : '本地文件'
}
/**
* 挑出「播放器该自动跳转到的那一集」。
*
* 传给 /play/:id 的可能是「整部剧」这类自身没有可播文件的条目——首页轮播、远程 Emby
* 剧集库的卡片、剧集详情页的播放按钮都会把剧集 ID 直接交给播放器。这种条目的选集接口
* 返回的是它下面的剧集,列表里没有它自己;若直接拿剧集 ID 去请求
* /api/stream/<剧集ID>,远程 Emby 会返回 HTTP 500(Unable to cast object of type
* 'Series' to type 'IHasMediaSources'),用户看到的就是「播放失败」。此时换成第一集,
* 播放、进度、上下集都落在真正可播的条目上。
*
* 返回 null 表示无需跳转:条目还没加载完、条目本身可播(含以其它版本形式出现在列表里)、
* 或该条目下没有任何可选剧集。
*/
export function autoPlayEpisodeFor(
media: Media | null | undefined,
requestedId: string,
episodes: Media[],
): Media | null {
if (!media || !requestedId || media.id !== requestedId) return null
if (episodes.length === 0) return null
if (episodes.some((episode) => mediaVersionMatches(episode, media.id))) return null
const first = episodes[0]
if (!first?.id || first.id === media.id) return null
return first
}
/** 版本文件名(去掉目录,并剥掉 .strm 外层扩展名),用作版本选项的次要说明。 */
export function mediaVersionFileName(media: Media): string {
const path = (media.path || '').replace(/\\/g, '/').trim()