添加即将上映
This commit is contained in:
@@ -296,6 +296,7 @@ interface SiteConfig {
|
||||
FluidSearch: boolean;
|
||||
DanmakuApiBase: string;
|
||||
DanmakuApiToken: string;
|
||||
TMDBApiKey?: string;
|
||||
EnableComments: boolean;
|
||||
EnableRegistration?: boolean;
|
||||
RegistrationRequireTurnstile?: boolean;
|
||||
@@ -4592,6 +4593,7 @@ const SiteConfigComponent = ({
|
||||
FluidSearch: true,
|
||||
DanmakuApiBase: 'http://localhost:9321',
|
||||
DanmakuApiToken: '87654321',
|
||||
TMDBApiKey: '',
|
||||
EnableComments: false,
|
||||
EnableRegistration: false,
|
||||
RegistrationRequireTurnstile: false,
|
||||
@@ -4674,6 +4676,7 @@ const SiteConfigComponent = ({
|
||||
DanmakuApiBase:
|
||||
config.SiteConfig.DanmakuApiBase || 'http://localhost:9321',
|
||||
DanmakuApiToken: config.SiteConfig.DanmakuApiToken || '87654321',
|
||||
TMDBApiKey: config.SiteConfig.TMDBApiKey || '',
|
||||
EnableComments: config.SiteConfig.EnableComments || false,
|
||||
EnableRegistration: config.SiteConfig.EnableRegistration || false,
|
||||
RegistrationRequireTurnstile: config.SiteConfig.RegistrationRequireTurnstile || false,
|
||||
@@ -5235,6 +5238,43 @@ const SiteConfigComponent = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TMDB 配置 */}
|
||||
<div className='space-y-4 pt-4 border-t border-gray-200 dark:border-gray-700'>
|
||||
<h3 className='text-sm font-semibold text-gray-900 dark:text-gray-100'>
|
||||
TMDB 配置
|
||||
</h3>
|
||||
|
||||
{/* TMDB API Key */}
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||
TMDB API Key
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
placeholder='请输入 TMDB API Key'
|
||||
value={siteSettings.TMDBApiKey}
|
||||
onChange={(e) =>
|
||||
setSiteSettings((prev) => ({
|
||||
...prev,
|
||||
TMDBApiKey: e.target.value,
|
||||
}))
|
||||
}
|
||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent'
|
||||
/>
|
||||
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
||||
配置后首页将显示 TMDB 即将上映电影。获取 API Key 请访问{' '}
|
||||
<a
|
||||
href='https://www.themoviedb.org/settings/api'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300'
|
||||
>
|
||||
TMDB API 设置页面
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 评论功能配置 */}
|
||||
<div className='space-y-4 pt-4 border-t border-gray-200 dark:border-gray-700'>
|
||||
<h3 className='text-sm font-semibold text-gray-900 dark:text-gray-100'>
|
||||
|
||||
@@ -41,6 +41,7 @@ export async function POST(request: NextRequest) {
|
||||
FluidSearch,
|
||||
DanmakuApiBase,
|
||||
DanmakuApiToken,
|
||||
TMDBApiKey,
|
||||
EnableComments,
|
||||
CustomAdFilterCode,
|
||||
CustomAdFilterVersion,
|
||||
@@ -72,6 +73,7 @@ export async function POST(request: NextRequest) {
|
||||
FluidSearch: boolean;
|
||||
DanmakuApiBase: string;
|
||||
DanmakuApiToken: string;
|
||||
TMDBApiKey?: string;
|
||||
EnableComments: boolean;
|
||||
CustomAdFilterCode?: string;
|
||||
CustomAdFilterVersion?: number;
|
||||
@@ -106,6 +108,7 @@ export async function POST(request: NextRequest) {
|
||||
typeof FluidSearch !== 'boolean' ||
|
||||
typeof DanmakuApiBase !== 'string' ||
|
||||
typeof DanmakuApiToken !== 'string' ||
|
||||
(TMDBApiKey !== undefined && typeof TMDBApiKey !== 'string') ||
|
||||
typeof EnableComments !== 'boolean' ||
|
||||
(CustomAdFilterCode !== undefined && typeof CustomAdFilterCode !== 'string') ||
|
||||
(CustomAdFilterVersion !== undefined && typeof CustomAdFilterVersion !== 'number') ||
|
||||
@@ -155,6 +158,7 @@ export async function POST(request: NextRequest) {
|
||||
FluidSearch,
|
||||
DanmakuApiBase,
|
||||
DanmakuApiToken,
|
||||
TMDBApiKey,
|
||||
EnableComments,
|
||||
CustomAdFilterCode,
|
||||
CustomAdFilterVersion,
|
||||
|
||||
@@ -61,6 +61,7 @@ export default async function RootLayout({
|
||||
process.env.NEXT_PUBLIC_DISABLE_YELLOW_FILTER === 'true';
|
||||
let fluidSearch = process.env.NEXT_PUBLIC_FLUID_SEARCH !== 'false';
|
||||
let enableComments = false;
|
||||
let tmdbApiKey = '';
|
||||
let customCategories = [] as {
|
||||
name: string;
|
||||
type: 'movie' | 'tv';
|
||||
@@ -85,6 +86,7 @@ export default async function RootLayout({
|
||||
}));
|
||||
fluidSearch = config.SiteConfig.FluidSearch;
|
||||
enableComments = config.SiteConfig.EnableComments;
|
||||
tmdbApiKey = config.SiteConfig.TMDBApiKey || '';
|
||||
}
|
||||
|
||||
// 将运行时配置注入到全局 window 对象,供客户端在运行时读取
|
||||
@@ -128,7 +130,7 @@ export default async function RootLayout({
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<SiteProvider siteName={siteName} announcement={announcement}>
|
||||
<SiteProvider siteName={siteName} announcement={announcement} tmdbApiKey={tmdbApiKey}>
|
||||
<WatchRoomProvider>
|
||||
<DownloadProvider>
|
||||
<DanmakuCacheCleanup />
|
||||
|
||||
@@ -18,6 +18,11 @@ import {
|
||||
subscribeToDataUpdates,
|
||||
} from '@/lib/db.client';
|
||||
import { getDoubanCategories } from '@/lib/douban.client';
|
||||
import {
|
||||
getTMDBUpcomingContent,
|
||||
getTMDBImageUrl,
|
||||
TMDBItem,
|
||||
} from '@/lib/tmdb.client';
|
||||
import { DoubanItem } from '@/lib/types';
|
||||
|
||||
import CapsuleSwitch from '@/components/CapsuleSwitch';
|
||||
@@ -32,11 +37,12 @@ function HomeClient() {
|
||||
const [hotMovies, setHotMovies] = useState<DoubanItem[]>([]);
|
||||
const [hotTvShows, setHotTvShows] = useState<DoubanItem[]>([]);
|
||||
const [hotVarietyShows, setHotVarietyShows] = useState<DoubanItem[]>([]);
|
||||
const [upcomingContent, setUpcomingContent] = useState<TMDBItem[]>([]);
|
||||
const [bangumiCalendarData, setBangumiCalendarData] = useState<
|
||||
BangumiCalendarData[]
|
||||
>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { announcement } = useSite();
|
||||
const { announcement, tmdbApiKey } = useSite();
|
||||
|
||||
const [showAnnouncement, setShowAnnouncement] = useState(false);
|
||||
|
||||
@@ -73,7 +79,7 @@ function HomeClient() {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// 并行获取热门电影、热门剧集和热门综艺
|
||||
// 并行获取热门电影、热门剧集、热门综艺和番剧日历
|
||||
const [moviesData, tvShowsData, varietyShowsData, bangumiCalendarData] =
|
||||
await Promise.all([
|
||||
getDoubanCategories({
|
||||
@@ -99,6 +105,20 @@ function HomeClient() {
|
||||
}
|
||||
|
||||
setBangumiCalendarData(bangumiCalendarData);
|
||||
|
||||
// 如果配置了 TMDB API Key,则获取即将上映/播出内容
|
||||
if (tmdbApiKey) {
|
||||
const tmdbData = await getTMDBUpcomingContent(tmdbApiKey);
|
||||
if (tmdbData.code === 200) {
|
||||
// 按上映/播出日期升序排序(最近的排在前面)
|
||||
const sortedContent = [...tmdbData.list].sort((a, b) => {
|
||||
const dateA = new Date(a.release_date || '9999-12-31').getTime();
|
||||
const dateB = new Date(b.release_date || '9999-12-31').getTime();
|
||||
return dateA - dateB;
|
||||
});
|
||||
setUpcomingContent(sortedContent);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取推荐数据失败:', error);
|
||||
} finally {
|
||||
@@ -107,7 +127,7 @@ function HomeClient() {
|
||||
};
|
||||
|
||||
fetchRecommendData();
|
||||
}, []);
|
||||
}, [tmdbApiKey]);
|
||||
|
||||
// 处理收藏数据更新的函数
|
||||
const updateFavoriteItems = useCallback(
|
||||
@@ -447,6 +467,40 @@ function HomeClient() {
|
||||
))}
|
||||
</ScrollableRow>
|
||||
</section>
|
||||
|
||||
{/* 即将上映/播出 (TMDB) */}
|
||||
{tmdbApiKey && upcomingContent.length > 0 && (
|
||||
<section className='mb-8'>
|
||||
<div className='mb-4 flex items-center justify-between'>
|
||||
<h2 className='text-xl font-bold text-gray-800 dark:text-gray-200'>
|
||||
即将上映
|
||||
</h2>
|
||||
</div>
|
||||
<ScrollableRow>
|
||||
{upcomingContent.map((item) => (
|
||||
<div
|
||||
key={`${item.media_type}-${item.id}`}
|
||||
className='min-w-[96px] w-24 sm:min-w-[180px] sm:w-44'
|
||||
>
|
||||
<VideoCard
|
||||
title={item.title}
|
||||
poster={getTMDBImageUrl(item.poster_path)}
|
||||
year={item.release_date?.split('-')?.[0] || ''}
|
||||
rate={
|
||||
item.vote_average && item.vote_average > 0
|
||||
? item.vote_average.toFixed(1)
|
||||
: ''
|
||||
}
|
||||
type={item.media_type === 'tv' ? 'tv' : 'movie'}
|
||||
from='douban'
|
||||
releaseDate={item.release_date}
|
||||
isUpcoming={true}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</ScrollableRow>
|
||||
</section>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2,11 +2,16 @@
|
||||
|
||||
import { createContext, ReactNode, useContext } from 'react';
|
||||
|
||||
const SiteContext = createContext<{ siteName: string; announcement?: string }>({
|
||||
const SiteContext = createContext<{
|
||||
siteName: string;
|
||||
announcement?: string;
|
||||
tmdbApiKey?: string;
|
||||
}>({
|
||||
// 默认值
|
||||
siteName: 'MoonTV',
|
||||
announcement:
|
||||
'本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。',
|
||||
tmdbApiKey: '',
|
||||
});
|
||||
|
||||
export const useSite = () => useContext(SiteContext);
|
||||
@@ -15,13 +20,15 @@ export function SiteProvider({
|
||||
children,
|
||||
siteName,
|
||||
announcement,
|
||||
tmdbApiKey,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
siteName: string;
|
||||
announcement?: string;
|
||||
tmdbApiKey?: string;
|
||||
}) {
|
||||
return (
|
||||
<SiteContext.Provider value={{ siteName, announcement }}>
|
||||
<SiteContext.Provider value={{ siteName, announcement, tmdbApiKey }}>
|
||||
{children}
|
||||
</SiteContext.Provider>
|
||||
);
|
||||
|
||||
@@ -47,6 +47,8 @@ export interface VideoCardProps {
|
||||
isBangumi?: boolean;
|
||||
isAggregate?: boolean;
|
||||
origin?: 'vod' | 'live';
|
||||
releaseDate?: string; // 上映日期,格式:YYYY-MM-DD
|
||||
isUpcoming?: boolean; // 是否为即将上映
|
||||
}
|
||||
|
||||
export type VideoCardHandle = {
|
||||
@@ -76,6 +78,8 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||
isBangumi = false,
|
||||
isAggregate = false,
|
||||
origin = 'vod',
|
||||
releaseDate,
|
||||
isUpcoming = false,
|
||||
}: VideoCardProps,
|
||||
ref
|
||||
) {
|
||||
@@ -223,6 +227,11 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||
);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
// 即将上映的电影不跳转
|
||||
if (isUpcoming) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (origin === 'live' && actualSource && actualId) {
|
||||
// 直播内容跳转到直播页面
|
||||
const url = `/live?source=${actualSource.replace('live_', '')}&id=${actualId.replace('live_', '')}`;
|
||||
@@ -240,6 +249,7 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||
router.push(url);
|
||||
}
|
||||
}, [
|
||||
isUpcoming,
|
||||
origin,
|
||||
from,
|
||||
actualSource,
|
||||
@@ -254,6 +264,11 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||
|
||||
// 新标签页播放处理函数
|
||||
const handlePlayInNewTab = useCallback(() => {
|
||||
// 即将上映的电影不跳转
|
||||
if (isUpcoming) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (origin === 'live' && actualSource && actualId) {
|
||||
// 直播内容跳转到直播页面
|
||||
const url = `/live?source=${actualSource.replace('live_', '')}&id=${actualId.replace('live_', '')}`;
|
||||
@@ -270,6 +285,7 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
}, [
|
||||
isUpcoming,
|
||||
origin,
|
||||
from,
|
||||
actualSource,
|
||||
@@ -295,6 +311,11 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||
|
||||
// 长按操作
|
||||
const handleLongPress = useCallback(() => {
|
||||
// 即将上映的电影不显示操作菜单
|
||||
if (isUpcoming) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!showMobileActions) { // 防止重复触发
|
||||
// 立即显示菜单,避免等待数据加载导致动画卡顿
|
||||
setShowMobileActions(true);
|
||||
@@ -304,7 +325,7 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||
checkSearchFavoriteStatus();
|
||||
}
|
||||
}
|
||||
}, [showMobileActions, from, isAggregate, actualSource, actualId, searchFavorited, checkSearchFavoriteStatus]);
|
||||
}, [isUpcoming, showMobileActions, from, isAggregate, actualSource, actualId, searchFavorited, checkSearchFavoriteStatus]);
|
||||
|
||||
// 长按手势hook
|
||||
const longPressProps = useLongPress({
|
||||
@@ -313,6 +334,28 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||
longPressDelay: 500,
|
||||
});
|
||||
|
||||
// 计算距离上映的天数(使用本地时区)
|
||||
const daysUntilRelease = useMemo(() => {
|
||||
if (!isUpcoming || !releaseDate) return null;
|
||||
|
||||
// 获取今天的本地日期(午夜)
|
||||
const today = new Date();
|
||||
const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
|
||||
|
||||
// 将日期字符串解析为本地时区的日期对象
|
||||
// 使用 'YYYY-MM-DD' 格式直接构造,避免 UTC 解析问题
|
||||
const [releaseYear, releaseMonth, releaseDay] = releaseDate.split('-').map(Number);
|
||||
const release = new Date(releaseYear, releaseMonth - 1, releaseDay);
|
||||
|
||||
const [todayYear, todayMonth, todayDay] = todayStr.split('-').map(Number);
|
||||
const todayDate = new Date(todayYear, todayMonth - 1, todayDay);
|
||||
|
||||
const diffTime = release.getTime() - todayDate.getTime();
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
return diffDays;
|
||||
}, [isUpcoming, releaseDate]);
|
||||
|
||||
const config = useMemo(() => {
|
||||
const configs = {
|
||||
playrecord: {
|
||||
@@ -348,7 +391,7 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||
douban: {
|
||||
showSourceName: false,
|
||||
showProgress: false,
|
||||
showPlayButton: true,
|
||||
showPlayButton: !isUpcoming, // 即将上映不显示播放按钮
|
||||
showHeart: false,
|
||||
showCheckCircle: false,
|
||||
showDoubanLink: true,
|
||||
@@ -357,7 +400,7 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||
},
|
||||
};
|
||||
return configs[from] || configs.search;
|
||||
}, [from, isAggregate, douban_id, rate]);
|
||||
}, [from, isAggregate, douban_id, rate, isUpcoming]);
|
||||
|
||||
// 移动端操作菜单配置
|
||||
const mobileActions = useMemo(() => {
|
||||
@@ -495,7 +538,7 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className='group relative w-full rounded-lg bg-transparent cursor-pointer transition-all duration-300 ease-in-out hover:scale-[1.05] hover:z-[500]'
|
||||
className={`group relative w-full rounded-lg bg-transparent transition-all duration-300 ease-in-out hover:scale-[1.05] hover:z-[500] ${isUpcoming ? 'cursor-default' : 'cursor-pointer'}`}
|
||||
onClick={handleClick}
|
||||
{...longPressProps}
|
||||
style={{
|
||||
@@ -513,6 +556,11 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// 即将上映的电影不显示操作菜单
|
||||
if (isUpcoming) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 右键弹出操作菜单
|
||||
setShowMobileActions(true);
|
||||
|
||||
@@ -595,8 +643,37 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 播放按钮 */}
|
||||
{config.showPlayButton && (
|
||||
{/* 播放按钮或上映倒计时 */}
|
||||
{isUpcoming && daysUntilRelease !== null ? (
|
||||
<div
|
||||
data-button="true"
|
||||
className='absolute inset-0 flex items-center justify-center opacity-0 transition-all duration-300 ease-in-out delay-75 group-hover:opacity-100'
|
||||
style={{
|
||||
WebkitUserSelect: 'none',
|
||||
userSelect: 'none',
|
||||
WebkitTouchCallout: 'none',
|
||||
} as React.CSSProperties}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className='bg-black/70 backdrop-blur-sm text-white px-4 py-2 rounded-lg text-sm font-medium shadow-lg'
|
||||
style={{
|
||||
WebkitUserSelect: 'none',
|
||||
userSelect: 'none',
|
||||
WebkitTouchCallout: 'none',
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
{daysUntilRelease > 0
|
||||
? `${daysUntilRelease}天后上映`
|
||||
: daysUntilRelease === 0
|
||||
? '今日上映'
|
||||
: '已上映'}
|
||||
</div>
|
||||
</div>
|
||||
) : config.showPlayButton && (
|
||||
<div
|
||||
data-button="true"
|
||||
className='absolute inset-0 flex items-center justify-center opacity-0 transition-all duration-300 ease-in-out delay-75 group-hover:opacity-100 group-hover:scale-100'
|
||||
|
||||
@@ -19,6 +19,8 @@ export interface AdminConfig {
|
||||
// 弹幕配置
|
||||
DanmakuApiBase: string;
|
||||
DanmakuApiToken: string;
|
||||
// TMDB配置
|
||||
TMDBApiKey?: string;
|
||||
// 评论功能开关
|
||||
EnableComments: boolean;
|
||||
// 自定义去广告代码
|
||||
|
||||
@@ -221,6 +221,8 @@ async function getInitConfig(configFile: string, subConfig: {
|
||||
// 弹幕配置
|
||||
DanmakuApiBase: process.env.DANMAKU_API_BASE || 'http://localhost:9321',
|
||||
DanmakuApiToken: process.env.DANMAKU_API_TOKEN || '87654321',
|
||||
// TMDB配置
|
||||
TMDBApiKey: '',
|
||||
// 评论功能开关
|
||||
EnableComments: false,
|
||||
},
|
||||
|
||||
201
src/lib/tmdb.client.ts
Normal file
201
src/lib/tmdb.client.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any,no-console */
|
||||
|
||||
export interface TMDBMovie {
|
||||
id: number;
|
||||
title: string;
|
||||
poster_path: string | null;
|
||||
release_date: string;
|
||||
overview: string;
|
||||
vote_average: number;
|
||||
}
|
||||
|
||||
export interface TMDBTVShow {
|
||||
id: number;
|
||||
name: string;
|
||||
poster_path: string | null;
|
||||
first_air_date: string;
|
||||
overview: string;
|
||||
vote_average: number;
|
||||
}
|
||||
|
||||
// 统一的类型,用于显示
|
||||
export interface TMDBItem {
|
||||
id: number;
|
||||
title: string;
|
||||
poster_path: string | null;
|
||||
release_date: string;
|
||||
overview: string;
|
||||
vote_average: number;
|
||||
media_type: 'movie' | 'tv';
|
||||
}
|
||||
|
||||
interface TMDBUpcomingResponse {
|
||||
results: TMDBMovie[];
|
||||
page: number;
|
||||
total_pages: number;
|
||||
total_results: number;
|
||||
}
|
||||
|
||||
interface TMDBTVAiringTodayResponse {
|
||||
results: TMDBTVShow[];
|
||||
page: number;
|
||||
total_pages: number;
|
||||
total_results: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取即将上映的电影
|
||||
* @param apiKey - TMDB API Key
|
||||
* @param page - 页码
|
||||
* @param region - 地区代码,默认 CN (中国)
|
||||
* @returns 即将上映的电影列表
|
||||
*/
|
||||
export async function getTMDBUpcomingMovies(
|
||||
apiKey: string,
|
||||
page: number = 1,
|
||||
region: string = 'CN'
|
||||
): Promise<{ code: number; list: TMDBMovie[] }> {
|
||||
try {
|
||||
if (!apiKey) {
|
||||
return { code: 400, list: [] };
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`https://api.themoviedb.org/3/movie/upcoming?api_key=${apiKey}&language=zh-CN&page=${page}®ion=${region}`
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('TMDB API 请求失败:', response.status, response.statusText);
|
||||
return { code: response.status, list: [] };
|
||||
}
|
||||
|
||||
const data: TMDBUpcomingResponse = await response.json();
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
list: data.results,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取 TMDB 即将上映电影失败:', error);
|
||||
return { code: 500, list: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取正在播出的电视剧
|
||||
* @param apiKey - TMDB API Key
|
||||
* @param page - 页码
|
||||
* @returns 正在播出的电视剧列表
|
||||
*/
|
||||
export async function getTMDBUpcomingTVShows(
|
||||
apiKey: string,
|
||||
page: number = 1
|
||||
): Promise<{ code: number; list: TMDBTVShow[] }> {
|
||||
try {
|
||||
if (!apiKey) {
|
||||
return { code: 400, list: [] };
|
||||
}
|
||||
|
||||
// 使用 on_the_air 接口获取正在播出的电视剧
|
||||
const response = await fetch(
|
||||
`https://api.themoviedb.org/3/tv/on_the_air?api_key=${apiKey}&language=zh-CN&page=${page}`
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('TMDB TV API 请求失败:', response.status, response.statusText);
|
||||
return { code: response.status, list: [] };
|
||||
}
|
||||
|
||||
const data: TMDBTVAiringTodayResponse = await response.json();
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
list: data.results,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取 TMDB 正在播出电视剧失败:', error);
|
||||
return { code: 500, list: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取即将上映/播出的内容(电影+电视剧)
|
||||
* @param apiKey - TMDB API Key
|
||||
* @returns 统一格式的即将上映/播出列表
|
||||
*/
|
||||
export async function getTMDBUpcomingContent(
|
||||
apiKey: string
|
||||
): Promise<{ code: number; list: TMDBItem[] }> {
|
||||
try {
|
||||
if (!apiKey) {
|
||||
return { code: 400, list: [] };
|
||||
}
|
||||
|
||||
// 并行获取电影和电视剧数据
|
||||
const [moviesResult, tvShowsResult] = await Promise.all([
|
||||
getTMDBUpcomingMovies(apiKey),
|
||||
getTMDBUpcomingTVShows(apiKey),
|
||||
]);
|
||||
|
||||
// 获取今天的日期(本地时区)
|
||||
const today = new Date();
|
||||
const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
|
||||
|
||||
// 转换电影数据为统一格式,并过滤掉已上映的
|
||||
const movies: TMDBItem[] = moviesResult.list
|
||||
.filter((movie) => {
|
||||
// 只保留未来上映的电影
|
||||
return movie.release_date && movie.release_date >= todayStr;
|
||||
})
|
||||
.map((movie) => ({
|
||||
id: movie.id,
|
||||
title: movie.title,
|
||||
poster_path: movie.poster_path,
|
||||
release_date: movie.release_date,
|
||||
overview: movie.overview,
|
||||
vote_average: movie.vote_average,
|
||||
media_type: 'movie' as const,
|
||||
}));
|
||||
|
||||
// 转换电视剧数据为统一格式,并过滤掉已播出的
|
||||
const tvShows: TMDBItem[] = tvShowsResult.list
|
||||
.filter((tv) => {
|
||||
// 只保留未来播出的电视剧
|
||||
return tv.first_air_date && tv.first_air_date >= todayStr;
|
||||
})
|
||||
.map((tv) => ({
|
||||
id: tv.id,
|
||||
title: tv.name,
|
||||
poster_path: tv.poster_path,
|
||||
release_date: tv.first_air_date,
|
||||
overview: tv.overview,
|
||||
vote_average: tv.vote_average,
|
||||
media_type: 'tv' as const,
|
||||
}));
|
||||
|
||||
// 合并并返回
|
||||
const allContent = [...movies, ...tvShows];
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
list: allContent,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取 TMDB 即将上映内容失败:', error);
|
||||
return { code: 500, list: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 TMDB 图片完整 URL
|
||||
* @param path - 图片路径
|
||||
* @param size - 图片尺寸,默认 w500
|
||||
* @returns 完整的图片 URL
|
||||
*/
|
||||
export function getTMDBImageUrl(
|
||||
path: string | null,
|
||||
size: string = 'w500'
|
||||
): string {
|
||||
if (!path) return '';
|
||||
return `https://image.tmdb.org/t/p/${size}${path}`;
|
||||
}
|
||||
Reference in New Issue
Block a user