mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-07 05:46:38 +08:00
fix: show latest discover posters
This commit is contained in:
@@ -25,9 +25,12 @@ type discoverSectionDef struct {
|
|||||||
var discoverSectionCatalog = []discoverSectionDef{
|
var discoverSectionCatalog = []discoverSectionDef{
|
||||||
{Key: "tmdb_trending_day", Label: "TMDb 今日趋势", Provider: "tmdb"},
|
{Key: "tmdb_trending_day", Label: "TMDb 今日趋势", Provider: "tmdb"},
|
||||||
{Key: "tmdb_trending_week", Label: "TMDb 本周热门", Provider: "tmdb"},
|
{Key: "tmdb_trending_week", Label: "TMDb 本周热门", Provider: "tmdb"},
|
||||||
|
{Key: "tmdb_latest_movie", Label: "TMDb 最新电影", Provider: "tmdb"},
|
||||||
|
{Key: "tmdb_latest_tv", Label: "TMDb 最新剧集", Provider: "tmdb"},
|
||||||
{Key: "tmdb_popular_movie", Label: "TMDb 热门电影", Provider: "tmdb"},
|
{Key: "tmdb_popular_movie", Label: "TMDb 热门电影", Provider: "tmdb"},
|
||||||
{Key: "tmdb_popular_tv", Label: "TMDb 热门剧集", Provider: "tmdb"},
|
{Key: "tmdb_popular_tv", Label: "TMDb 热门剧集", Provider: "tmdb"},
|
||||||
{Key: "tmdb_top_rated_movie", Label: "TMDb 高分电影", Provider: "tmdb"},
|
{Key: "tmdb_top_rated_movie", Label: "TMDb 高分电影", Provider: "tmdb"},
|
||||||
|
{Key: "tmdb_upcoming_movie", Label: "TMDb 即将上映", Provider: "tmdb"},
|
||||||
{Key: "douban_hot_movie", Label: "豆瓣热门电影", Provider: "douban"},
|
{Key: "douban_hot_movie", Label: "豆瓣热门电影", Provider: "douban"},
|
||||||
{Key: "douban_hot_tv", Label: "豆瓣热门剧集", Provider: "douban"},
|
{Key: "douban_hot_tv", Label: "豆瓣热门剧集", Provider: "douban"},
|
||||||
{Key: "douban_top_movie", Label: "豆瓣高分电影", Provider: "douban"},
|
{Key: "douban_top_movie", Label: "豆瓣高分电影", Provider: "douban"},
|
||||||
@@ -94,7 +97,7 @@ func enabledDiscoverSections(ctx context.Context, svc *service.Container) []disc
|
|||||||
}
|
}
|
||||||
|
|
||||||
func defaultDiscoverSectionKeys(ctx context.Context, svc *service.Container) []string {
|
func defaultDiscoverSectionKeys(ctx context.Context, svc *service.Container) []string {
|
||||||
preferred := []string{"tmdb_trending_day", "douban_hot_movie", "douban_hot_tv", "bangumi_calendar"}
|
preferred := []string{"tmdb_trending_day", "tmdb_latest_movie", "tmdb_latest_tv", "douban_hot_movie", "douban_hot_tv", "bangumi_calendar"}
|
||||||
enabled := map[string]struct{}{}
|
enabled := map[string]struct{}{}
|
||||||
for _, section := range enabledDiscoverSections(ctx, svc) {
|
for _, section := range enabledDiscoverSections(ctx, svc) {
|
||||||
enabled[section.Key] = struct{}{}
|
enabled[section.Key] = struct{}{}
|
||||||
@@ -124,7 +127,7 @@ func discoverSectionProvider(key string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch key {
|
switch key {
|
||||||
case "trending_day", "trending_week", "popular_movie", "popular_tv", "top_rated_movie", "upcoming_movie":
|
case "trending_day", "trending_week", "latest_movie", "latest_tv", "popular_movie", "popular_tv", "top_rated_movie", "upcoming_movie":
|
||||||
return "tmdb"
|
return "tmdb"
|
||||||
default:
|
default:
|
||||||
return ""
|
return ""
|
||||||
@@ -144,8 +147,8 @@ func discoverProviderEnabled(ctx context.Context, svc *service.Container, provid
|
|||||||
|
|
||||||
func discoverSectionItems(ctx context.Context, svc *service.Container, k string) ([]service.ExternalMediaResult, error) {
|
func discoverSectionItems(ctx context.Context, svc *service.Container, k string) ([]service.ExternalMediaResult, error) {
|
||||||
switch k {
|
switch k {
|
||||||
case "tmdb_trending_day", "tmdb_trending_week", "tmdb_popular_movie", "tmdb_popular_tv", "tmdb_top_rated_movie",
|
case "tmdb_trending_day", "tmdb_trending_week", "tmdb_latest_movie", "tmdb_latest_tv", "tmdb_popular_movie", "tmdb_popular_tv", "tmdb_top_rated_movie", "tmdb_upcoming_movie",
|
||||||
"trending_day", "trending_week", "popular_movie", "popular_tv", "top_rated_movie", "upcoming_movie":
|
"trending_day", "trending_week", "latest_movie", "latest_tv", "popular_movie", "popular_tv", "top_rated_movie", "upcoming_movie":
|
||||||
return svc.Discover.TMDbSection(ctx, k)
|
return svc.Discover.TMDbSection(ctx, k)
|
||||||
case "douban_hot_movie", "douban_hot_tv", "douban_top_movie":
|
case "douban_hot_movie", "douban_hot_tv", "douban_top_movie":
|
||||||
if svc.Douban == nil {
|
if svc.Douban == nil {
|
||||||
|
|||||||
@@ -65,3 +65,16 @@ func TestDefaultDiscoverSectionKeysSkipDisabledProviders(t *testing.T) {
|
|||||||
t.Fatal("default keys should keep enabled providers")
|
t.Fatal("default keys should keep enabled providers")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDefaultDiscoverSectionKeysIncludeLatestTMDbRails(t *testing.T) {
|
||||||
|
keys := defaultDiscoverSectionKeys(t.Context(), &service.Container{})
|
||||||
|
keySet := map[string]struct{}{}
|
||||||
|
for _, key := range keys {
|
||||||
|
keySet[key] = struct{}{}
|
||||||
|
}
|
||||||
|
for _, key := range []string{"tmdb_latest_movie", "tmdb_latest_tv"} {
|
||||||
|
if _, ok := keySet[key]; !ok {
|
||||||
|
t.Fatalf("default discover keys should include %q: %v", key, keys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -178,6 +178,10 @@ func tmdbDiscoverPath(key string) string {
|
|||||||
return "/trending/movie/day"
|
return "/trending/movie/day"
|
||||||
case "tmdb_trending_week", "trending_week":
|
case "tmdb_trending_week", "trending_week":
|
||||||
return "/trending/movie/week"
|
return "/trending/movie/week"
|
||||||
|
case "tmdb_latest_movie", "latest_movie":
|
||||||
|
return "/movie/now_playing"
|
||||||
|
case "tmdb_latest_tv", "latest_tv":
|
||||||
|
return "/tv/on_the_air"
|
||||||
case "tmdb_popular_movie", "popular_movie":
|
case "tmdb_popular_movie", "popular_movie":
|
||||||
return "/movie/popular"
|
return "/movie/popular"
|
||||||
case "tmdb_popular_tv", "popular_tv":
|
case "tmdb_popular_tv", "popular_tv":
|
||||||
|
|||||||
@@ -83,6 +83,21 @@ func TestDiscoverWarmArtworkNoImageProxyIsNoop(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTMDbDiscoverPathIncludesLatestSections(t *testing.T) {
|
||||||
|
cases := map[string]string{
|
||||||
|
"tmdb_latest_movie": "/movie/now_playing",
|
||||||
|
"latest_movie": "/movie/now_playing",
|
||||||
|
"tmdb_latest_tv": "/tv/on_the_air",
|
||||||
|
"latest_tv": "/tv/on_the_air",
|
||||||
|
"tmdb_upcoming_movie": "/movie/upcoming",
|
||||||
|
}
|
||||||
|
for key, want := range cases {
|
||||||
|
if got := tmdbDiscoverPath(key); got != want {
|
||||||
|
t.Fatalf("tmdbDiscoverPath(%q) = %q, want %q", key, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func waitForDiscoverArtworkCache(t *testing.T, calls *int32, wantCalls int32, cachePath, raw string) {
|
func waitForDiscoverArtworkCache(t *testing.T, calls *int32, wantCalls int32, cachePath, raw string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
deadline := time.Now().Add(2 * time.Second)
|
deadline := time.Now().Add(2 * time.Second)
|
||||||
|
|||||||
@@ -65,34 +65,53 @@ function DiscoverCard({
|
|||||||
onSelect: (item: DiscoverItem) => void
|
onSelect: (item: DiscoverItem) => void
|
||||||
}) {
|
}) {
|
||||||
const source = discoverItemSource(item)
|
const source = discoverItemSource(item)
|
||||||
|
const imageCandidates = useMemo(
|
||||||
|
() =>
|
||||||
|
[item.poster_url, item.backdrop_url]
|
||||||
|
.map((value) => value?.trim())
|
||||||
|
.filter((value, index, values): value is string => Boolean(value) && values.indexOf(value) === index),
|
||||||
|
[item.poster_url, item.backdrop_url],
|
||||||
|
)
|
||||||
|
const [imageIndex, setImageIndex] = useState(0)
|
||||||
const [posterRetry, setPosterRetry] = useState(0)
|
const [posterRetry, setPosterRetry] = useState(0)
|
||||||
const [posterUnavailable, setPosterUnavailable] = useState(false)
|
const [posterUnavailable, setPosterUnavailable] = useState(false)
|
||||||
const posterVersion = [imageVersion, posterRetry > 0 ? `r${posterRetry}` : ''].filter(Boolean).join('-')
|
const posterVersion = [imageVersion, posterRetry > 0 ? `r${posterRetry}` : ''].filter(Boolean).join('-')
|
||||||
|
const activeImage = imageCandidates[imageIndex] ?? ''
|
||||||
const shouldRefreshCache = Boolean(
|
const shouldRefreshCache = Boolean(
|
||||||
(imageVersion && refreshImageVersion === imageVersion) || posterRetry > 0,
|
(imageVersion && refreshImageVersion === imageVersion) || posterRetry > 0,
|
||||||
)
|
)
|
||||||
const posterSrc = useMemo(
|
const posterSrc = useMemo(
|
||||||
() =>
|
() =>
|
||||||
imageURL(item.poster_url, posterVersion, {
|
imageURL(activeImage, posterVersion, {
|
||||||
refreshCache: shouldRefreshCache,
|
refreshCache: shouldRefreshCache,
|
||||||
retryFailed: true,
|
retryFailed: true,
|
||||||
}),
|
}),
|
||||||
[item.poster_url, posterVersion, shouldRefreshCache],
|
[activeImage, posterVersion, shouldRefreshCache],
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
setImageIndex(0)
|
||||||
setPosterRetry(0)
|
setPosterRetry(0)
|
||||||
setPosterUnavailable(false)
|
setPosterUnavailable(false)
|
||||||
}, [item.poster_url])
|
}, [item.poster_url, item.backdrop_url, imageVersion])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!posterUnavailable || posterRetry >= 3) return
|
if (!posterUnavailable) return
|
||||||
|
if (imageIndex + 1 < imageCandidates.length) {
|
||||||
|
const timer = window.setTimeout(() => {
|
||||||
|
setImageIndex((current) => Math.min(current + 1, imageCandidates.length - 1))
|
||||||
|
setPosterRetry(0)
|
||||||
|
setPosterUnavailable(false)
|
||||||
|
}, 150)
|
||||||
|
return () => window.clearTimeout(timer)
|
||||||
|
}
|
||||||
|
if (posterRetry >= 3) return
|
||||||
const timer = window.setTimeout(() => {
|
const timer = window.setTimeout(() => {
|
||||||
setPosterRetry((current) => current + 1)
|
setPosterRetry((current) => current + 1)
|
||||||
setPosterUnavailable(false)
|
setPosterUnavailable(false)
|
||||||
}, 1200 * (posterRetry + 1))
|
}, 1200 * (posterRetry + 1))
|
||||||
return () => window.clearTimeout(timer)
|
return () => window.clearTimeout(timer)
|
||||||
}, [posterRetry, posterUnavailable])
|
}, [imageCandidates.length, imageIndex, posterRetry, posterUnavailable])
|
||||||
|
|
||||||
const markPosterUnavailable = () => setPosterUnavailable(true)
|
const markPosterUnavailable = () => setPosterUnavailable(true)
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
defaultSections,
|
defaultSections,
|
||||||
discoverStorageKey,
|
discoverStorageKey,
|
||||||
readSavedSections,
|
readSavedSections,
|
||||||
|
serializeSavedSections,
|
||||||
} from './discoverPageModel'
|
} from './discoverPageModel'
|
||||||
|
|
||||||
export function DiscoverPage() {
|
export function DiscoverPage() {
|
||||||
@@ -74,7 +75,7 @@ export function DiscoverPage() {
|
|||||||
}
|
}
|
||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
window.localStorage.setItem(discoverStorageKey, JSON.stringify(selected))
|
window.localStorage.setItem(discoverStorageKey, serializeSavedSections(selected))
|
||||||
|
|
||||||
let pending = selected.length
|
let pending = selected.length
|
||||||
const markDone = () => {
|
const markDone = () => {
|
||||||
|
|||||||
@@ -2,15 +2,21 @@ import type { DiscoverItem, DiscoverSection } from '../api/discover'
|
|||||||
|
|
||||||
export const defaultSections = [
|
export const defaultSections = [
|
||||||
'tmdb_trending_day',
|
'tmdb_trending_day',
|
||||||
|
'tmdb_latest_movie',
|
||||||
|
'tmdb_latest_tv',
|
||||||
'douban_hot_movie',
|
'douban_hot_movie',
|
||||||
'douban_hot_tv',
|
'douban_hot_tv',
|
||||||
'bangumi_calendar',
|
'bangumi_calendar',
|
||||||
]
|
]
|
||||||
|
|
||||||
export const discoverStorageKey = 'mediastation.discover.sections'
|
export const discoverStorageKey = 'mediastation.discover.sections'
|
||||||
|
const discoverStorageVersion = 2
|
||||||
|
const legacyDefaultAdditions = ['tmdb_latest_movie', 'tmdb_latest_tv']
|
||||||
|
|
||||||
export const defaultSectionDefs: DiscoverSection[] = [
|
export const defaultSectionDefs: DiscoverSection[] = [
|
||||||
{ key: 'tmdb_trending_day', label: 'TMDb 今日趋势', provider: 'tmdb' },
|
{ key: 'tmdb_trending_day', label: 'TMDb 今日趋势', provider: 'tmdb' },
|
||||||
|
{ key: 'tmdb_latest_movie', label: 'TMDb 最新电影', provider: 'tmdb' },
|
||||||
|
{ key: 'tmdb_latest_tv', label: 'TMDb 最新剧集', provider: 'tmdb' },
|
||||||
{ key: 'tmdb_popular_movie', label: 'TMDb 热门电影', provider: 'tmdb' },
|
{ key: 'tmdb_popular_movie', label: 'TMDb 热门电影', provider: 'tmdb' },
|
||||||
{ key: 'douban_hot_movie', label: '豆瓣热门电影', provider: 'douban' },
|
{ key: 'douban_hot_movie', label: '豆瓣热门电影', provider: 'douban' },
|
||||||
{ key: 'douban_hot_tv', label: '豆瓣热门剧集', provider: 'douban' },
|
{ key: 'douban_hot_tv', label: '豆瓣热门剧集', provider: 'douban' },
|
||||||
@@ -26,14 +32,44 @@ export function readSavedSections(sections: DiscoverSection[]): string[] {
|
|||||||
const raw = window.localStorage.getItem(discoverStorageKey)
|
const raw = window.localStorage.getItem(discoverStorageKey)
|
||||||
if (!raw) return []
|
if (!raw) return []
|
||||||
const parsed = JSON.parse(raw)
|
const parsed = JSON.parse(raw)
|
||||||
if (!Array.isArray(parsed)) return []
|
|
||||||
const allowed = new Set(sections.map((section) => section.key))
|
const allowed = new Set(sections.map((section) => section.key))
|
||||||
return parsed.filter((key) => typeof key === 'string' && allowed.has(key))
|
if (Array.isArray(parsed)) {
|
||||||
|
return orderSectionKeys(addLegacyDefaults(parsed, allowed), sections)
|
||||||
|
}
|
||||||
|
if (!parsed || !Array.isArray(parsed.selected)) return []
|
||||||
|
const selected = sanitizeSectionKeys(parsed.selected, allowed)
|
||||||
|
if (parsed.version === discoverStorageVersion) {
|
||||||
|
return orderSectionKeys(selected, sections)
|
||||||
|
}
|
||||||
|
return orderSectionKeys(addLegacyDefaults(selected, allowed), sections)
|
||||||
} catch {
|
} catch {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function serializeSavedSections(selected: string[]): string {
|
||||||
|
return JSON.stringify({ version: discoverStorageVersion, selected })
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeSectionKeys(keys: unknown[], allowed: Set<string>): string[] {
|
||||||
|
return keys.filter((key): key is string => typeof key === 'string' && allowed.has(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
function addLegacyDefaults(keys: unknown[], allowed: Set<string>): string[] {
|
||||||
|
const out = sanitizeSectionKeys(keys, allowed)
|
||||||
|
for (const key of legacyDefaultAdditions) {
|
||||||
|
if (allowed.has(key) && !out.includes(key)) {
|
||||||
|
out.push(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
function orderSectionKeys(keys: string[], sections: DiscoverSection[]): string[] {
|
||||||
|
const selected = new Set(keys)
|
||||||
|
return sections.map((section) => section.key).filter((key) => selected.has(key))
|
||||||
|
}
|
||||||
|
|
||||||
export function buildSubscribeKeyword(item: DiscoverItem): string {
|
export function buildSubscribeKeyword(item: DiscoverItem): string {
|
||||||
return [item.title, item.year && item.year > 0 ? item.year : ''].filter(Boolean).join(' ')
|
return [item.title, item.year && item.year > 0 ? item.year : ''].filter(Boolean).join(' ')
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user