mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 11:16:37 +08:00
优化
This commit is contained in:
@@ -301,11 +301,22 @@ func (r *EmbyRemoteService) MapRemoteItemToMedia(ctx context.Context, mount *mod
|
|||||||
media.SeasonNum = 0
|
media.SeasonNum = 0
|
||||||
media.EpisodeNum = 0
|
media.EpisodeNum = 0
|
||||||
}
|
}
|
||||||
if mount != nil && strings.TrimSpace(mount.RemoteViewID) != "" {
|
if mount != nil && strings.TrimSpace(mount.RemoteViewID) != "" {
|
||||||
libID := EncodeEmbyRemoteID(mount.ID, mount.RemoteViewID)
|
libID := EncodeEmbyRemoteID(mount.ID, mount.RemoteViewID)
|
||||||
media.DisplayLibraryID = libID
|
media.DisplayLibraryID = libID
|
||||||
media.LibraryID = libID
|
media.LibraryID = libID
|
||||||
}
|
libName := strings.TrimSpace(mount.Name)
|
||||||
|
if libName == "" {
|
||||||
|
libName = strings.TrimSpace(mount.RemoteViewName)
|
||||||
|
}
|
||||||
|
if libName == "" && acct != nil {
|
||||||
|
libName = acct.Name
|
||||||
|
} else if acct != nil && acct.Name != "" && !strings.Contains(libName, acct.Name) {
|
||||||
|
libName = acct.Name + " · " + libName
|
||||||
|
}
|
||||||
|
media.LibraryName = libName
|
||||||
|
media.DisplayLibraryName = libName
|
||||||
|
}
|
||||||
return media
|
return media
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -120,6 +120,11 @@ func GroupMediaVersions(items []model.Media) []MediaItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func mediaVersionGroupKey(m model.Media) string {
|
func mediaVersionGroupKey(m model.Media) string {
|
||||||
|
// 远程 Emby 挂载条目保持独立,不与其它远程条目或本地条目折叠合并。
|
||||||
|
if IsEmbyRemoteID(m.ID) {
|
||||||
|
return fmt.Sprintf("embyremote:%s", m.ID)
|
||||||
|
}
|
||||||
|
|
||||||
if m.SeasonNum > 0 || m.EpisodeNum > 0 {
|
if m.SeasonNum > 0 || m.EpisodeNum > 0 {
|
||||||
switch {
|
switch {
|
||||||
case m.TMDbID > 0:
|
case m.TMDbID > 0:
|
||||||
@@ -146,14 +151,32 @@ func mediaVersionGroupKey(m model.Media) string {
|
|||||||
fmt.Sprintf("%d:%d", m.SeasonNum, m.EpisodeNum),
|
fmt.Sprintf("%d:%d", m.SeasonNum, m.EpisodeNum),
|
||||||
}, "|")
|
}, "|")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
libKey := strings.ToLower(strings.TrimSpace(m.LibraryID))
|
||||||
|
if libKey == "" {
|
||||||
|
libKey = strings.ToLower(strings.TrimSpace(m.DisplayLibraryID))
|
||||||
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case m.TMDbID > 0:
|
case m.TMDbID > 0:
|
||||||
|
if libKey != "" {
|
||||||
|
return fmt.Sprintf("movie:%s:tmdb:%d", libKey, m.TMDbID)
|
||||||
|
}
|
||||||
return fmt.Sprintf("tmdb:%d", m.TMDbID)
|
return fmt.Sprintf("tmdb:%d", m.TMDbID)
|
||||||
case m.BangumiID > 0:
|
case m.BangumiID > 0:
|
||||||
|
if libKey != "" {
|
||||||
|
return fmt.Sprintf("movie:%s:bangumi:%d", libKey, m.BangumiID)
|
||||||
|
}
|
||||||
return fmt.Sprintf("bangumi:%d", m.BangumiID)
|
return fmt.Sprintf("bangumi:%d", m.BangumiID)
|
||||||
case strings.TrimSpace(m.DoubanID) != "":
|
case strings.TrimSpace(m.DoubanID) != "":
|
||||||
|
if libKey != "" {
|
||||||
|
return fmt.Sprintf("movie:%s:douban:%s", libKey, strings.ToLower(strings.TrimSpace(m.DoubanID)))
|
||||||
|
}
|
||||||
return "douban:" + strings.ToLower(strings.TrimSpace(m.DoubanID))
|
return "douban:" + strings.ToLower(strings.TrimSpace(m.DoubanID))
|
||||||
case strings.TrimSpace(m.TheTVDBID) != "":
|
case strings.TrimSpace(m.TheTVDBID) != "":
|
||||||
|
if libKey != "" {
|
||||||
|
return fmt.Sprintf("movie:%s:thetvdb:%s", libKey, strings.ToLower(strings.TrimSpace(m.TheTVDBID)))
|
||||||
|
}
|
||||||
return "thetvdb:" + strings.ToLower(strings.TrimSpace(m.TheTVDBID))
|
return "thetvdb:" + strings.ToLower(strings.TrimSpace(m.TheTVDBID))
|
||||||
}
|
}
|
||||||
title := firstNonEmpty(m.OriginalName, m.Title)
|
title := firstNonEmpty(m.OriginalName, m.Title)
|
||||||
@@ -173,6 +196,9 @@ func mediaVersionGroupKey(m model.Media) string {
|
|||||||
if year <= 0 {
|
if year <= 0 {
|
||||||
_, year = CleanQuery(m.Path)
|
_, year = CleanQuery(m.Path)
|
||||||
}
|
}
|
||||||
|
if libKey != "" {
|
||||||
|
return fmt.Sprintf("movie:%s:%s:%d", libKey, title, year)
|
||||||
|
}
|
||||||
return fmt.Sprintf("movie:%s:%d", title, year)
|
return fmt.Sprintf("movie:%s:%d", title, year)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -218,3 +218,49 @@ func TestSearchMediaVisiblePageGroupedPaginatesAfterVersionGrouping(t *testing.T
|
|||||||
t.Fatalf("primary version = %q, want %q", page[0].Media.Path, rows[0].Path)
|
t.Fatalf("primary version = %q, want %q", page[0].Media.Path, rows[0].Path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGroupMediaVersionsDoesNotMergeDifferentRemoteSeries(t *testing.T) {
|
||||||
|
// 模拟来自两个不同挂载库(如 00 新番连载 与 2018 动漫)的同名剧集《碧蓝之海》
|
||||||
|
season1 := model.Media{
|
||||||
|
Base: model.Base{ID: EncodeEmbyRemoteID("mount-1", "156019")},
|
||||||
|
LibraryID: EncodeEmbyRemoteID("mount-1", "view-1"),
|
||||||
|
Title: "碧蓝之海",
|
||||||
|
Year: 2018,
|
||||||
|
LibraryName: "2018 动漫",
|
||||||
|
}
|
||||||
|
season2 := model.Media{
|
||||||
|
Base: model.Base{ID: EncodeEmbyRemoteID("mount-2", "156030")},
|
||||||
|
LibraryID: EncodeEmbyRemoteID("mount-2", "view-2"),
|
||||||
|
Title: "碧蓝之海",
|
||||||
|
Year: 2024,
|
||||||
|
LibraryName: "00 新番连载",
|
||||||
|
}
|
||||||
|
|
||||||
|
grouped := groupMediaVersions([]model.Media{season1, season2})
|
||||||
|
if len(grouped) != 2 {
|
||||||
|
t.Fatalf("expected 2 separate groups for different remote series/libraries, got %d: %#v", len(grouped), grouped)
|
||||||
|
}
|
||||||
|
if grouped[0].Title != "碧蓝之海" || grouped[1].Title != "碧蓝之海" {
|
||||||
|
t.Fatalf("expected both titles to be '碧蓝之海'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGroupMediaVersionsDoesNotMergeAcrossDifferentLibrariesForMovies(t *testing.T) {
|
||||||
|
movieLib1 := model.Media{
|
||||||
|
Base: model.Base{ID: "m-1"},
|
||||||
|
LibraryID: "lib-1",
|
||||||
|
Title: "碧蓝之海",
|
||||||
|
Year: 2018,
|
||||||
|
}
|
||||||
|
movieLib2 := model.Media{
|
||||||
|
Base: model.Base{ID: "m-2"},
|
||||||
|
LibraryID: "lib-2",
|
||||||
|
Title: "碧蓝之海",
|
||||||
|
Year: 2018,
|
||||||
|
}
|
||||||
|
|
||||||
|
grouped := groupMediaVersions([]model.Media{movieLib1, movieLib2})
|
||||||
|
if len(grouped) != 2 {
|
||||||
|
t.Fatalf("expected 2 separate groups for movies in different libraries, got %d", len(grouped))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -239,9 +239,16 @@ function LayoutHeaderSearch() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="truncate text-xs font-bold text-[var(--app-text)] group-hover:text-brand-500">
|
<div className="flex items-center gap-1.5">
|
||||||
{item.title}
|
<p className="truncate text-xs font-bold text-[var(--app-text)] group-hover:text-brand-500">
|
||||||
</p>
|
{item.title}
|
||||||
|
</p>
|
||||||
|
{(item.display_library_name || item.library_name) && (
|
||||||
|
<span className="shrink-0 text-[9px] px-1.5 py-0.5 rounded bg-[var(--app-panel-elevated)] border border-[var(--app-border)] text-[var(--app-muted)] max-w-[130px] truncate">
|
||||||
|
{item.display_library_name || item.library_name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="flex items-center gap-2 text-[10px] text-[var(--app-muted)] mt-0.5">
|
<div className="flex items-center gap-2 text-[10px] text-[var(--app-muted)] mt-0.5">
|
||||||
{item.year > 0 && <span>{item.year}</span>}
|
{item.year > 0 && <span>{item.year}</span>}
|
||||||
{item.rating > 0 && (
|
{item.rating > 0 && (
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
MediaDetailMainContent,
|
MediaDetailMainContent,
|
||||||
MediaDetailMissing,
|
MediaDetailMissing,
|
||||||
} from './MediaDetailPageSections'
|
} from './MediaDetailPageSections'
|
||||||
|
import { LibrarySeriesEpisodes } from './LibrarySeriesEpisodes'
|
||||||
import { useMediaDetailPageState } from './useMediaDetailPageState'
|
import { useMediaDetailPageState } from './useMediaDetailPageState'
|
||||||
|
|
||||||
export function MediaDetailPage() {
|
export function MediaDetailPage() {
|
||||||
@@ -31,6 +32,7 @@ export function MediaDetailPage() {
|
|||||||
media={media}
|
media={media}
|
||||||
isAdmin={role === 'admin'}
|
isAdmin={role === 'admin'}
|
||||||
favourite={detail.favourite}
|
favourite={detail.favourite}
|
||||||
|
playTargetId={detail.firstPlayableEpisode?.id}
|
||||||
scrapeEpisodeArtwork={detail.scrapeEpisodeArtwork}
|
scrapeEpisodeArtwork={detail.scrapeEpisodeArtwork}
|
||||||
onToggleFavourite={detail.toggleFavourite}
|
onToggleFavourite={detail.toggleFavourite}
|
||||||
onScrapeEpisodeArtworkChange={detail.setScrapeEpisodeArtwork}
|
onScrapeEpisodeArtworkChange={detail.setScrapeEpisodeArtwork}
|
||||||
@@ -42,6 +44,21 @@ export function MediaDetailPage() {
|
|||||||
onExportNFO={detail.exportNFO}
|
onExportNFO={detail.exportNFO}
|
||||||
onSoftDelete={detail.softDelete}
|
onSoftDelete={detail.softDelete}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{detail.hasEpisodes && (
|
||||||
|
<div className="relative z-10 px-6 pb-8 sm:px-10 space-y-6">
|
||||||
|
<div className="divider border-gray-200/60" />
|
||||||
|
<LibrarySeriesEpisodes
|
||||||
|
loading={detail.loadingEpisodes}
|
||||||
|
selectedEpisodes={detail.seasonGroups}
|
||||||
|
selectedSeason={detail.selectedSeason}
|
||||||
|
visibleEpisodes={detail.visibleEpisodes}
|
||||||
|
playbackFrom={`/media/${media.id}`}
|
||||||
|
onSeasonChange={detail.setSelectedSeason}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<MediaDetailDialogs
|
<MediaDetailDialogs
|
||||||
media={media}
|
media={media}
|
||||||
manualScrapeOpen={detail.manualScrapeOpen}
|
manualScrapeOpen={detail.manualScrapeOpen}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ interface MediaDetailPlaybackActionsProps {
|
|||||||
media: Media
|
media: Media
|
||||||
favourite: boolean
|
favourite: boolean
|
||||||
onToggleFavourite: () => void
|
onToggleFavourite: () => void
|
||||||
|
playTargetId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MediaDetailMainContentProps extends MediaDetailPlaybackActionsProps {
|
interface MediaDetailMainContentProps extends MediaDetailPlaybackActionsProps {
|
||||||
@@ -93,17 +94,19 @@ export function MediaDetailPlaybackActions({
|
|||||||
media,
|
media,
|
||||||
favourite,
|
favourite,
|
||||||
onToggleFavourite,
|
onToggleFavourite,
|
||||||
|
playTargetId,
|
||||||
}: MediaDetailPlaybackActionsProps) {
|
}: MediaDetailPlaybackActionsProps) {
|
||||||
|
const targetId = playTargetId || media.id
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
<Link to={`/play/${media.id}`} state={{ from: `/media/${media.id}` }} className="btn-primary px-6 py-3.5 shadow-sm">
|
<Link to={`/play/${targetId}`} state={{ from: `/media/${media.id}` }} className="btn-primary px-6 py-3.5 shadow-sm">
|
||||||
<Play size={16} fill="currentColor" />
|
<Play size={16} fill="currentColor" />
|
||||||
<span>立即播放</span>
|
<span>立即播放</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{!isDirectStreamMedia(media) && (
|
{!isDirectStreamMedia(media) && (
|
||||||
<Link
|
<Link
|
||||||
to={`/play/${media.id}?mode=hls`}
|
to={`/play/${targetId}?mode=hls`}
|
||||||
state={{ from: `/media/${media.id}` }}
|
state={{ from: `/media/${media.id}` }}
|
||||||
className="btn-outline border-brand-500/30 hover:border-brand-500 text-[#c9954a] hover:bg-brand-50 px-5"
|
className="btn-outline border-brand-500/30 hover:border-brand-500 text-[#c9954a] hover:bg-brand-50 px-5"
|
||||||
>
|
>
|
||||||
@@ -112,7 +115,7 @@ export function MediaDetailPlaybackActions({
|
|||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ExternalPlayerButton mediaId={media.id} />
|
<ExternalPlayerButton mediaId={targetId} />
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={onToggleFavourite}
|
onClick={onToggleFavourite}
|
||||||
@@ -134,6 +137,7 @@ export function MediaDetailMainContent({
|
|||||||
media,
|
media,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
favourite,
|
favourite,
|
||||||
|
playTargetId,
|
||||||
scrapeEpisodeArtwork,
|
scrapeEpisodeArtwork,
|
||||||
onToggleFavourite,
|
onToggleFavourite,
|
||||||
onScrapeEpisodeArtworkChange,
|
onScrapeEpisodeArtworkChange,
|
||||||
@@ -153,7 +157,12 @@ export function MediaDetailMainContent({
|
|||||||
<MediaDetailMetadata media={media} />
|
<MediaDetailMetadata media={media} />
|
||||||
<div className="divider border-gray-200/60" />
|
<div className="divider border-gray-200/60" />
|
||||||
<div className="flex flex-col gap-5">
|
<div className="flex flex-col gap-5">
|
||||||
<MediaDetailPlaybackActions media={media} favourite={favourite} onToggleFavourite={onToggleFavourite} />
|
<MediaDetailPlaybackActions
|
||||||
|
media={media}
|
||||||
|
favourite={favourite}
|
||||||
|
onToggleFavourite={onToggleFavourite}
|
||||||
|
playTargetId={playTargetId}
|
||||||
|
/>
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<MediaDetailAdminPanel
|
<MediaDetailAdminPanel
|
||||||
media={media}
|
media={media}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useState, type Dispatch, type SetStateAction } from 'react'
|
import { useCallback, useEffect, useMemo, useState, type Dispatch, type SetStateAction } from 'react'
|
||||||
import type { NavigateFunction } from 'react-router-dom'
|
import type { NavigateFunction } from 'react-router-dom'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
@@ -20,6 +20,8 @@ interface MediaDetailRefreshParams {
|
|||||||
setMedia: Dispatch<SetStateAction<Media | null>>
|
setMedia: Dispatch<SetStateAction<Media | null>>
|
||||||
setFavourite: Dispatch<SetStateAction<boolean>>
|
setFavourite: Dispatch<SetStateAction<boolean>>
|
||||||
setLoading: Dispatch<SetStateAction<boolean>>
|
setLoading: Dispatch<SetStateAction<boolean>>
|
||||||
|
setEpisodes: Dispatch<SetStateAction<Media[]>>
|
||||||
|
setLoadingEpisodes: Dispatch<SetStateAction<boolean>>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MediaDetailActionsParams {
|
interface MediaDetailActionsParams {
|
||||||
@@ -34,12 +36,23 @@ export function useMediaDetailPageState({ id, navigate }: MediaDetailPageStatePa
|
|||||||
const [media, setMedia] = useState<Media | null>(null)
|
const [media, setMedia] = useState<Media | null>(null)
|
||||||
const [favourite, setFavourite] = useState(false)
|
const [favourite, setFavourite] = useState(false)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [episodes, setEpisodes] = useState<Media[]>([])
|
||||||
|
const [loadingEpisodes, setLoadingEpisodes] = useState(false)
|
||||||
|
const [selectedSeason, setSelectedSeason] = useState<number | null>(null)
|
||||||
|
|
||||||
const [manualScrapeOpen, setManualScrapeOpen] = useState(false)
|
const [manualScrapeOpen, setManualScrapeOpen] = useState(false)
|
||||||
const [metadataEditOpen, setMetadataEditOpen] = useState(false)
|
const [metadataEditOpen, setMetadataEditOpen] = useState(false)
|
||||||
const [organizeOpen, setOrganizeOpen] = useState(false)
|
const [organizeOpen, setOrganizeOpen] = useState(false)
|
||||||
const [scrapeEpisodeArtwork, setScrapeEpisodeArtwork] = useState(false)
|
const [scrapeEpisodeArtwork, setScrapeEpisodeArtwork] = useState(false)
|
||||||
|
|
||||||
const refresh = useMediaDetailRefresh({ id, setMedia, setFavourite, setLoading })
|
const refresh = useMediaDetailRefresh({
|
||||||
|
id,
|
||||||
|
setMedia,
|
||||||
|
setFavourite,
|
||||||
|
setLoading,
|
||||||
|
setEpisodes,
|
||||||
|
setLoadingEpisodes,
|
||||||
|
})
|
||||||
const actions = useMediaDetailActions({
|
const actions = useMediaDetailActions({
|
||||||
media,
|
media,
|
||||||
scrapeEpisodeArtwork,
|
scrapeEpisodeArtwork,
|
||||||
@@ -52,6 +65,41 @@ export function useMediaDetailPageState({ id, navigate }: MediaDetailPageStatePa
|
|||||||
refresh().catch(() => undefined)
|
refresh().catch(() => undefined)
|
||||||
}, [refresh])
|
}, [refresh])
|
||||||
|
|
||||||
|
const seasonGroups = useMemo(() => {
|
||||||
|
if (episodes.length === 0) return []
|
||||||
|
const seasons = new Map<number, Media[]>()
|
||||||
|
for (const ep of episodes) {
|
||||||
|
const s = ep.episode_num > 0 ? (ep.season_num ?? 0) : (ep.season_num || 1)
|
||||||
|
if (!seasons.has(s)) seasons.set(s, [])
|
||||||
|
seasons.get(s)!.push(ep)
|
||||||
|
}
|
||||||
|
for (const [, list] of seasons) {
|
||||||
|
list.sort((a, b) => (a.episode_num || 0) - (b.episode_num || 0))
|
||||||
|
}
|
||||||
|
return Array.from(seasons.entries())
|
||||||
|
.sort(([a], [b]) => a - b)
|
||||||
|
.map(([season, list]) => ({ season, episodes: list }))
|
||||||
|
}, [episodes])
|
||||||
|
|
||||||
|
const visibleEpisodes = useMemo(() => {
|
||||||
|
if (selectedSeason == null) return seasonGroups[0]?.episodes ?? []
|
||||||
|
return seasonGroups.find((s) => s.season === selectedSeason)?.episodes ?? []
|
||||||
|
}, [seasonGroups, selectedSeason])
|
||||||
|
|
||||||
|
const hasEpisodes = useMemo(() => {
|
||||||
|
if (episodes.length > 1) return true
|
||||||
|
if (episodes.length === 1 && media) {
|
||||||
|
// 只有单条时,如果明确是集(有 episode_num > 0)或 ID 与自身不同,则按剧集渲染
|
||||||
|
return episodes[0].episode_num > 0 || episodes[0].id !== media.id
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}, [episodes, media])
|
||||||
|
|
||||||
|
const firstPlayableEpisode = useMemo(() => {
|
||||||
|
if (episodes.length === 0) return null
|
||||||
|
return (visibleEpisodes.length > 0 ? visibleEpisodes[0] : episodes[0]) || null
|
||||||
|
}, [episodes, visibleEpisodes])
|
||||||
|
|
||||||
const handleMetadataSaved = useCallback(async (next: Media) => {
|
const handleMetadataSaved = useCallback(async (next: Media) => {
|
||||||
setMedia(next)
|
setMedia(next)
|
||||||
await refresh()
|
await refresh()
|
||||||
@@ -61,6 +109,14 @@ export function useMediaDetailPageState({ id, navigate }: MediaDetailPageStatePa
|
|||||||
media,
|
media,
|
||||||
favourite,
|
favourite,
|
||||||
loading,
|
loading,
|
||||||
|
episodes,
|
||||||
|
loadingEpisodes,
|
||||||
|
seasonGroups,
|
||||||
|
selectedSeason,
|
||||||
|
visibleEpisodes,
|
||||||
|
hasEpisodes,
|
||||||
|
firstPlayableEpisode,
|
||||||
|
setSelectedSeason,
|
||||||
manualScrapeOpen,
|
manualScrapeOpen,
|
||||||
metadataEditOpen,
|
metadataEditOpen,
|
||||||
organizeOpen,
|
organizeOpen,
|
||||||
@@ -80,19 +136,31 @@ function useMediaDetailRefresh({
|
|||||||
setMedia,
|
setMedia,
|
||||||
setFavourite,
|
setFavourite,
|
||||||
setLoading,
|
setLoading,
|
||||||
|
setEpisodes,
|
||||||
|
setLoadingEpisodes,
|
||||||
}: MediaDetailRefreshParams): () => Promise<void> {
|
}: MediaDetailRefreshParams): () => Promise<void> {
|
||||||
return useCallback(async () => {
|
return useCallback(async () => {
|
||||||
if (!id) return
|
if (!id) return
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
setLoadingEpisodes(true)
|
||||||
try {
|
try {
|
||||||
const nextMedia = await mediaAPI.get(id)
|
const nextMedia = await mediaAPI.get(id)
|
||||||
setMedia(nextMedia)
|
setMedia(nextMedia)
|
||||||
const favourites = await playbackAPI.listFavourites().catch(() => [])
|
const favourites = await playbackAPI.listFavourites().catch(() => [])
|
||||||
setFavourite(favourites.some((item) => item.id === nextMedia.id))
|
setFavourite(favourites.some((item) => item.id === nextMedia.id))
|
||||||
|
// 异步加载分集列表
|
||||||
|
try {
|
||||||
|
const epRes = await mediaAPI.getEpisodes(id)
|
||||||
|
setEpisodes(epRes.items ?? [])
|
||||||
|
} catch {
|
||||||
|
setEpisodes([])
|
||||||
|
} finally {
|
||||||
|
setLoadingEpisodes(false)
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}, [id, setFavourite, setLoading, setMedia])
|
}, [id, setFavourite, setLoading, setMedia, setEpisodes, setLoadingEpisodes])
|
||||||
}
|
}
|
||||||
|
|
||||||
function useMediaDetailActions({
|
function useMediaDetailActions({
|
||||||
|
|||||||
Reference in New Issue
Block a user