feat(adapters): TMDB 列表与筛选、豆瓣榜单 / 标签 / 筛选 / 搜索、bgm 浏览与趋势,豆瓣 / bgm 请求缓存端口
- TMDB:流行趋势(日 / 周)、电影 / 剧集榜单、discover 参数映射、分页搜索;列表类请求 TTL 6 小时 - 豆瓣 / bgm:列表与搜索请求经 MetaRequestCache 与限流,失败(403 / 风控页)不缓存 - contracts:发现页条目、分页、查询校验与筛选字典;storage.cacheTtl 追加 meta* 字段 - testkit:mock TMDB 加列表接口 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
1 parent
27514f4985
commit
e46eb0e3c3
13 files changed
+1344
-91
No files matched your search
@@ -56,6 +56,8 @@ export interface TmdbFixture {
|
||||
seasons?: TmdbFixtureSeason[];
|
||||
/** 海报路径(拼 image.tmdb.org);缺省 null */
|
||||
poster_path?: string | null;
|
||||
/** 列表接口(trending / discover / 榜单)返回的 genre_ids;缺省空 */
|
||||
genre_ids?: number[];
|
||||
/** 剧集组(`/tv/{id}/episode_groups`、`/tv/episode_group/{id}`);组内集按 TMDB 默认季集号引用 seasons 里的集 */
|
||||
episode_groups?: TmdbFixtureEpisodeGroup[];
|
||||
}
|
||||
@@ -82,6 +84,7 @@ export const TMDB_FIXTURES: TmdbFixture[] = [
|
||||
{
|
||||
type: "movie",
|
||||
id: 872585,
|
||||
genre_ids: [18, 36],
|
||||
original_title: "Oppenheimer",
|
||||
original_language: "en",
|
||||
origin_country: ["US"],
|
||||
@@ -100,6 +103,7 @@ export const TMDB_FIXTURES: TmdbFixture[] = [
|
||||
{
|
||||
type: "movie",
|
||||
id: 693134,
|
||||
genre_ids: [878, 12],
|
||||
original_title: "Dune: Part Two",
|
||||
original_language: "en",
|
||||
origin_country: ["US"],
|
||||
@@ -116,6 +120,7 @@ export const TMDB_FIXTURES: TmdbFixture[] = [
|
||||
{
|
||||
type: "movie",
|
||||
id: 842675,
|
||||
genre_ids: [878, 28],
|
||||
original_title: "流浪地球2",
|
||||
original_language: "zh",
|
||||
origin_country: ["CN"],
|
||||
@@ -131,6 +136,7 @@ export const TMDB_FIXTURES: TmdbFixture[] = [
|
||||
{
|
||||
type: "tv",
|
||||
id: 136315,
|
||||
genre_ids: [35, 18],
|
||||
original_title: "The Bear",
|
||||
original_language: "en",
|
||||
origin_country: ["US"],
|
||||
@@ -181,6 +187,7 @@ export const TMDB_FIXTURES: TmdbFixture[] = [
|
||||
{
|
||||
type: "tv",
|
||||
id: 100088,
|
||||
genre_ids: [18, 10765],
|
||||
original_title: "The Last of Us",
|
||||
original_language: "en",
|
||||
origin_country: ["US"],
|
||||
@@ -209,6 +216,7 @@ export const TMDB_FIXTURES: TmdbFixture[] = [
|
||||
{
|
||||
type: "tv",
|
||||
id: 900001,
|
||||
genre_ids: [10764],
|
||||
original_title: "向往的生活",
|
||||
original_language: "zh",
|
||||
origin_country: ["CN"],
|
||||
@@ -260,6 +268,7 @@ export const TMDB_FIXTURES: TmdbFixture[] = [
|
||||
{
|
||||
type: "tv",
|
||||
id: 95396,
|
||||
genre_ids: [18, 9648],
|
||||
original_title: "Severance",
|
||||
original_language: "en",
|
||||
origin_country: ["US"],
|
||||
|
||||
@@ -19,7 +19,10 @@ const notFound: MockReply = {
|
||||
json: { success: false, status_code: 34, status_message: "The resource you requested could not be found." },
|
||||
};
|
||||
|
||||
/** mock TMDB v3:/3/search/{movie,tv}、/3/find/:imdbId、/3/{movie,tv}/:id(append_to_response)、/3/tv/:id/season/:n、/3/tv/:id/episode_groups、/3/tv/episode_group/:id */
|
||||
/** 列表接口一页的条数(同 TMDB) */
|
||||
const LIST_PAGE = 20;
|
||||
|
||||
/** mock TMDB v3:/3/trending、/3/discover、电影 / 剧集榜单(按热度,一页 20 条)、/3/search/{movie,tv}、/3/find/:imdbId、/3/{movie,tv}/:id(append_to_response)、/3/tv/:id/season/:n、/3/tv/:id/episode_groups、/3/tv/episode_group/:id */
|
||||
export async function createMockTmdb(opts: MockTmdbOptions = {}): Promise<MockTmdb> {
|
||||
const fx = opts.fixtures ?? TMDB_FIXTURES;
|
||||
const apiKey = opts.apiKey ?? "test-tmdb-key";
|
||||
@@ -56,8 +59,60 @@ export async function createMockTmdb(opts: MockTmdbOptions = {}): Promise<MockTm
|
||||
};
|
||||
};
|
||||
|
||||
const listItem = (f: TmdbFixture, lang?: string) => ({
|
||||
...brief(f, lang),
|
||||
genre_ids: f.genre_ids ?? [],
|
||||
vote_average: f.rich?.vote_average ?? 0,
|
||||
vote_count: f.rich?.vote_count ?? 0,
|
||||
});
|
||||
const listPage = (rows: TmdbFixture[], q: Record<string, string>, withType: boolean) => {
|
||||
const page = Math.max(1, Number(q.page ?? 1));
|
||||
const sorted = [...rows].sort((a, b) => b.popularity - a.popularity);
|
||||
const results = sorted
|
||||
.slice((page - 1) * LIST_PAGE, page * LIST_PAGE)
|
||||
.map((f) => ({ ...listItem(f, q.language), ...(withType ? { media_type: f.type } : {}) }));
|
||||
return {
|
||||
json: {
|
||||
page,
|
||||
results,
|
||||
total_pages: Math.max(1, Math.ceil(sorted.length / LIST_PAGE)),
|
||||
total_results: sorted.length,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const http = await startMockHttp(
|
||||
[
|
||||
[
|
||||
/^\/3\/trending\/(all|movie|tv)\/(day|week)$/,
|
||||
(m, q) => listPage(m[1] === "all" ? fx : fx.filter((f) => f.type === m[1]), q, true),
|
||||
],
|
||||
[
|
||||
/^\/3\/(movie|tv)\/(popular|now_playing|upcoming|top_rated|on_the_air|airing_today)$/,
|
||||
(m, q) =>
|
||||
listPage(
|
||||
fx.filter((f) => f.type === m[1]),
|
||||
q,
|
||||
false,
|
||||
),
|
||||
],
|
||||
[
|
||||
/^\/3\/discover\/(movie|tv)$/,
|
||||
(m, q) => {
|
||||
const year = q.primary_release_year ?? q.first_air_date_year;
|
||||
const gte = q["primary_release_date.gte"] ?? q["first_air_date.gte"];
|
||||
const lte = q["primary_release_date.lte"] ?? q["first_air_date.lte"];
|
||||
const rows = fx
|
||||
.filter((f) => f.type === m[1])
|
||||
.filter((f) => !q.with_genres || (f.genre_ids ?? []).includes(Number(q.with_genres)))
|
||||
.filter((f) => !q.with_original_language || f.original_language === q.with_original_language)
|
||||
.filter((f) => !q.with_origin_country || f.origin_country.includes(q.with_origin_country))
|
||||
.filter((f) => !year || f.date.startsWith(year))
|
||||
.filter((f) => (!gte || f.date >= gte) && (!lte || f.date <= lte))
|
||||
.filter((f) => !q["vote_average.gte"] || (f.rich?.vote_average ?? 0) >= Number(q["vote_average.gte"]));
|
||||
return listPage(rows, q, false);
|
||||
},
|
||||
],
|
||||
[
|
||||
/^\/3\/search\/multi$/,
|
||||
(_m, q) => {
|
||||
|
||||
Reference in new issue
Block a user