fix: map real app feed and signed media responses

This commit is contained in:
Zhou Weidong committed 2026-11-10 16:07:18 +08:00
1 parent 1e1e3cd434
commit 1b6ca4c339
4 files changed
+11 -5

No files matched your search

+1 -1
View File
@@ -9,5 +9,5 @@ import { appApi } from '../services/appApi';
import { normalizeDrama, normalizePage } from '../services/normalizers'; import { normalizeDrama, normalizePage } from '../services/normalizers';
import { useApi } from '../hooks/useApi'; import { useApi } from '../hooks/useApi';
export function HomePage() { const { locale } = useAppStore(); const [genre, setGenre] = useState(locale.genres[0]); const [toast, setToast] = useState(''); const api = useApi(() => appApi.dramas().then((payload) => normalizePage(payload).list.map((item) => normalizeDrama(item, locale.code))), [locale.code]); const remoteItems = api.data || []; const items = remoteItems.length ? remoteItems : searchCatalog('', genre, locale.genres.indexOf(genre)); const play = () => { setToast(locale.watch); setTimeout(() => setToast(''), 1400); }; return <><section className="hero-card"><div className="hero-copy"><small>YMB ORIGINAL</small><h1>{locale.hero}<br /><em>{locale.accent}</em></h1><p>{locale.description}</p><button className="primary-button" onClick={play}>▶ {locale.watch}</button></div><div className="hero-art"><img src="/assets/hero.jpg" alt="YMB drama" /></div></section><section><SectionHeading title={locale.picks} /><div className="chip-row">{locale.genres.map((item) => <button className={`chip ${genre === item ? 'active' : ''}`} key={item} onClick={() => setGenre(item)}>{item}</button>)}</div>{api.loading && <div className="api-note">{locale.loading}</div>}{api.error && <div className="api-note warning">{locale.apiFallback}</div>}<div className="video-grid">{items.map((video) => <MediaCard key={video.id} video={video} />)}</div></section><section><SectionHeading title={locale.shortTitle} action="Explore →" /><div className="shorts-row">{videos.slice(1, 7).map((video) => <MediaCard key={video.id} video={video} compact />)}</div></section>{toast && <div className="toast">{toast}</div>}</>; export function HomePage() { const { locale } = useAppStore(); const [genre, setGenre] = useState(locale.genres[0]); const [toast, setToast] = useState(''); const api = useApi(() => appApi.homeFeed().then((payload) => payload?.sections?.flatMap((section) => section.dramas || []).map((item) => normalizeDrama(item, locale.code)) || []), [locale.code]); const remoteItems = api.data || []; const items = remoteItems.length ? remoteItems : searchCatalog('', genre, locale.genres.indexOf(genre)); const play = () => { setToast(locale.watch); setTimeout(() => setToast(''), 1400); }; return <><section className="hero-card"><div className="hero-copy"><small>YMB ORIGINAL</small><h1>{locale.hero}<br /><em>{locale.accent}</em></h1><p>{locale.description}</p><button className="primary-button" onClick={play}>▶ {locale.watch}</button></div><div className="hero-art"><img src="/assets/hero.jpg" alt="YMB drama" /></div></section><section><SectionHeading title={locale.picks} /><div className="chip-row">{locale.genres.map((item) => <button className={`chip ${genre === item ? 'active' : ''}`} key={item} onClick={() => setGenre(item)}>{item}</button>)}</div>{api.loading && <div className="api-note">{locale.loading}</div>}{api.error && <div className="api-note warning">{locale.apiFallback}</div>}<div className="video-grid">{items.map((video) => <MediaCard key={video.id} video={video} />)}</div></section><section><SectionHeading title={locale.shortTitle} action="Explore →" /><div className="shorts-row">{(remoteItems.length ? remoteItems.slice(0, 6) : videos.slice(1, 7)).map((video) => <MediaCard key={video.id} video={video} compact />)}</div></section>{toast && <div className="toast">{toast}</div>}</>;
} }
+2 -1
View File
@@ -7,5 +7,6 @@ import { appApi } from '../services/appApi';
import { normalizeDrama } from '../services/normalizers'; import { normalizeDrama } from '../services/normalizers';
import { useApi } from '../hooks/useApi'; import { useApi } from '../hooks/useApi';
import { VideoPlayer } from '../components/player/VideoPlayer'; import { VideoPlayer } from '../components/player/VideoPlayer';
import { resolvePlaybackUrl } from '../services/normalizers';
export function WatchPage() { const { id } = useParams(); const { locale } = useAppStore(); const fallback = videos.find((item) => item.id === id) || videos[0]; const drama = useApi(() => appApi.drama(id).then((data) => normalizeDrama(data, locale.code)), [id, locale.code]); const video = drama.data || fallback; const episode = video.episodes?.[0]; const playback = useApi(() => episode?.id ? appApi.episodePlay(episode.id) : Promise.resolve(null), [episode?.id]); const signedUrl = playback.data?.playUrl || playback.data?.url || playback.data?.signedUrl; return <div className="watch-layout"><div><div className="player">{signedUrl ? <VideoPlayer src={signedUrl} poster={video.image} /> : <><img src={video.image?.startsWith('/') ? video.image : `/assets/${video.image}`} alt={video.title} /><div className="player-state">{playback.loading ? locale.loading : locale.playbackUnavailable}</div></>}</div><h1 className="watch-title">{video.title}</h1><p className="watch-subtitle">{video.channel} · {video.views}</p>{playback.error && <div className="api-note warning">{playback.error}</div>}</div><aside className="up-next"><h2>{locale.picks}</h2>{videos.slice(0, 4).map((item) => <MediaCard key={item.id} video={item} />)}</aside></div>; } export function WatchPage() { const { id } = useParams(); const { locale } = useAppStore(); const fallback = videos.find((item) => item.id === id) || videos[0]; const drama = useApi(() => appApi.drama(id).then((data) => normalizeDrama(data, locale.code)), [id, locale.code]); const video = drama.data || fallback; const episode = video.episodes?.[0]; const playback = useApi(() => episode?.id ? appApi.episodePlay(episode.id) : Promise.resolve(null), [episode?.id]); const signedUrl = resolvePlaybackUrl(playback.data?.playUrl || playback.data?.url || playback.data?.signedUrl); return <div className="watch-layout"><div><div className="player">{signedUrl ? <VideoPlayer src={signedUrl} poster={video.image} /> : <><img src={video.image?.startsWith('/') ? video.image : `/assets/${video.image}`} alt={video.title} /><div className="player-state">{playback.loading ? locale.loading : locale.playbackUnavailable}</div></>}</div><h1 className="watch-title">{video.title}</h1><p className="watch-subtitle">{video.channel} · {video.views}</p>{playback.error && <div className="api-note warning">{playback.error}</div>}</div><aside className="up-next"><h2>{locale.picks}</h2>{videos.slice(0, 4).map((item) => <MediaCard key={item.id} video={item} />)}</aside></div>; }
+7 -2
View File
@@ -1,14 +1,19 @@
// Author: fruit.com.cn // Author: fruit.com.cn
const FALLBACK_IMAGE = '/assets/hero.jpg'; const FALLBACK_IMAGE = '/assets/hero.jpg';
const MEDIA_BASE_URL = (import.meta.env.VITE_MEDIA_BASE_URL || '').replace(/\/$/, '');
const getText = (value, lang = 'id') => (typeof value === 'string' ? value : value?.[lang] || value?.en || value?.id || value?.zh || ''); const getText = (value, lang = 'id') => (typeof value === 'string' ? value : value?.[lang] || value?.en || value?.id || value?.zh || '');
const getImage = (item) => item.coverUrl || item.cover || item.posterUrl || item.imageUrl || FALLBACK_IMAGE; const resolveMedia = (value) => value ? (/^https?:\/\//.test(value) ? value : `${MEDIA_BASE_URL}${value}`) : FALLBACK_IMAGE;
const getImage = (item) => resolveMedia(item.coverLandscape || item.coverUrl || item.cover || item.posterUrl || item.imageUrl);
export function normalizeDrama(item, lang = 'id') { export function normalizeDrama(item, lang = 'id') {
const id = String(item.id ?? item.dramaId ?? item.code ?? ''); const id = String(item.id ?? item.dramaId ?? item.code ?? '');
return { id, title: getText(item.locales || item.title, lang) || item.name || 'YMB Drama', channel: getText(item.channelLocales || item.channel, lang) || item.creatorName || 'YMB Originals', genre: getText(item.categoryLocales || item.genre, lang), image: getImage(item), duration: item.duration || '', views: item.views || item.viewCount || '', episodes: item.episodes || item.episodeList || [] }; const episodes = item.episodes || item.episodeList || (item.preview ? [item.preview] : []);
return { id, title: getText(item.titleLocales || item.locales || item.title, lang) || item.name || 'YMB Drama', channel: getText(item.channelLocales || item.channel, lang) || item.creatorName || 'YMB Originals', genre: getText(item.categoryLocales || item.genre, lang) || getText(item.types?.[0], lang), image: getImage(item), duration: item.duration || item.preview?.duration || '', views: item.views || item.viewCount || '', episodes, preview: item.preview ? { ...item.preview, playUrl: resolveMedia(item.preview.playUrl) } : null };
} }
export function normalizePage(payload) { export function normalizePage(payload) {
if (Array.isArray(payload)) return { total: payload.length, list: payload }; if (Array.isArray(payload)) return { total: payload.length, list: payload };
return { total: payload?.total || 0, list: payload?.list || payload?.records || [] }; return { total: payload?.total || 0, list: payload?.list || payload?.records || [] };
} }
export function resolvePlaybackUrl(value) { return resolveMedia(value); }
+1 -1
View File
@@ -2,4 +2,4 @@
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
export default defineConfig({ plugins: [react()], server: { proxy: { '/app': { target: 'http://127.0.0.1:8081', changeOrigin: true } } } }); export default defineConfig({ plugins: [react()], server: { proxy: { '/app': { target: 'http://127.0.0.1:8081', changeOrigin: true }, '/files': { target: 'http://127.0.0.1:8081', changeOrigin: true } } } });