feat(server): scrapeMedia 刮削任务,入库后自动刮削(SCR-1)
- 文件道任务 scrapeMedia:NFO 原子写入、图片经图片缓存下载(并发 4,失败不阻塞 NFO)、只补缺失 / 全部覆盖
- 设置 organize.scrapeAfterImport / scrapeMode / scrapeImages / scrapeEpisodeThumbs
- POST /library/{id}/scrape、详情 scrape 状态;files/scrape writeNfo 与 scrape-batch write 走同一任务
- 迁移 0057_media_scrape、原因码、审计 organize.scrape
- 与 MP 输出的 NFO 差分测试(apps/server/test/scrape)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
861f799615
commit
f38eca419e
24 files changed
+1740
-15
No files matched your search
@@ -305,8 +305,18 @@ describe.skipIf(skip)("files 识别 / 整理 / 探测 / 刮削", () => {
|
||||
|
||||
const s = await h.api("POST", "/files/scrape", { idempotencyKey: key(), mediaEntityId: film, writeNfo: true });
|
||||
expect(s.status).toBe(200);
|
||||
expect(s.body.data).toMatchObject({ mediaEntityId: film, nfo: "not_implemented", images: "not_requested" });
|
||||
// SCR-1:writeNfo 另排 scrapeMedia(写 NFO 与图片到媒体库目录)
|
||||
expect(s.body.data).toMatchObject({ mediaEntityId: film, nfo: "queued", images: "not_requested" });
|
||||
expect(s.body.data.scrapeTaskId).toBeTruthy();
|
||||
expect(h.engine.enqueued.some((e) => e.task === "refreshMetadata")).toBe(true);
|
||||
expect(h.engine.enqueued.some((e) => e.task === "scrapeMedia")).toBe(true);
|
||||
const b = await h.api("POST", "/files/scrape-batch", {
|
||||
idempotencyKey: key(),
|
||||
mediaEntityIds: [film],
|
||||
write: true,
|
||||
});
|
||||
expect(b.status).toBe(200);
|
||||
expect(b.body.data.submitted).toHaveLength(1);
|
||||
const none = await h.api("POST", "/files/scrape", { idempotencyKey: key(), mediaEntityId: show });
|
||||
expect(none.status).toBe(400);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
"""SCR-1 刮削差分:用 MP 上游的 TmdbScraper(themoviedb/scraper.py)从同一份 TMDB 原始响应生成 NFO,写 scrape-mp.json。
|
||||
|
||||
用法(MP 仓库根目录,用其 .venv):
|
||||
cd /opt/moviepilot-plus/upstream/MoviePilot && PYTHONPATH=. \
|
||||
.venv/bin/python /opt/uhd_media_tool/apps/server/test/scrape/gen-scrape-mp.py
|
||||
|
||||
只记录行为输出,不复制 MP 源码。MediaInfo 用 MP 自己的 set_tmdb_info(TMDB 详情 → 统一字段),
|
||||
季 / 集直接喂 TMDB 季详情(与 MP get_metadata_nfo 取到的一致)。图片命名按 MP 代码行为登记在 nfo-mp-diff.test.ts。
|
||||
"""
|
||||
|
||||
import json
|
||||
import pathlib
|
||||
|
||||
from app.domain.context import MediaInfo
|
||||
from app.domain.projection.tmdb import configure_image_url_builder
|
||||
from app.modules.themoviedb.scraper import TmdbScraper
|
||||
|
||||
HERE = pathlib.Path(__file__).parent
|
||||
raw = json.loads((HERE / "tmdb-raw.json").read_text(encoding="utf-8"))
|
||||
|
||||
# 启动组合根里注入的 TMDB 图片地址(settings.TMDB_IMAGE_URL,原图)
|
||||
configure_image_url_builder(lambda p: f"https://image.tmdb.org/t/p/original/{p.removeprefix('/')}")
|
||||
|
||||
s = TmdbScraper()
|
||||
|
||||
|
||||
def xml(doc) -> str:
|
||||
return doc.toprettyxml(indent=" ", encoding="utf-8").decode("utf-8")
|
||||
|
||||
|
||||
def media(info: dict, media_type: str) -> MediaInfo:
|
||||
mi = MediaInfo()
|
||||
mi.set_tmdb_info({**info, "media_type": media_type})
|
||||
return mi
|
||||
|
||||
|
||||
out = {}
|
||||
out["movie"] = xml(s._TmdbScraper__gen_movie_nfo_file(mediainfo=media(raw["/movie/157336"], "movie")))
|
||||
out["tvshow"] = xml(s._TmdbScraper__gen_tv_nfo_file(mediainfo=media(raw["/tv/95396"], "tv")))
|
||||
season = raw["/tv/95396/season/1"]
|
||||
out["season1"] = xml(s._TmdbScraper__gen_tv_season_nfo_file(seasoninfo=season, season=1))
|
||||
for ep in season["episodes"]:
|
||||
n = ep["episode_number"]
|
||||
out[f"s01e{n:02d}"] = xml(
|
||||
s._TmdbScraper__gen_tv_episode_nfo_file(tmdbid=95396, episodeinfo=ep, season=1, episode=n)
|
||||
)
|
||||
out["seasonPosterName"] = TmdbScraper.get_season_poster(season, 1)[0]
|
||||
out["seasonSpecialsPosterName"] = TmdbScraper.get_season_poster({"poster_path": "/x.jpg"}, 0)[0]
|
||||
|
||||
(HERE / "scrape-mp.json").write_text(json.dumps(out, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
print(f"写入 {len(out)} 项 → scrape-mp.json")
|
||||
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"_note": "SCR-1 NFO 与 MP 的已登记差异(按样例、按元素)。测试先去掉双方的空元素(MP 缺值写 <x/>、空 CDATA、评分 0,我们不写),再去掉这里登记的元素后逐行比较;登记的元素必须确实不同,差异消除后删掉登记。",
|
||||
"deviations": [
|
||||
{
|
||||
"case": "movie",
|
||||
"tag": "studio",
|
||||
"reason": "本地不存 TMDB production_companies,电影不写出品公司(Emby / Jellyfin 刷新时会自己补)"
|
||||
},
|
||||
{
|
||||
"case": "movie",
|
||||
"tag": "country",
|
||||
"reason": "制片国取 TMDB origin_country(代码转中文名);MP 取 production_countries(本例多一个英国)"
|
||||
},
|
||||
{
|
||||
"case": "movie",
|
||||
"tag": "tagline",
|
||||
"reason": "我们额外写标语,MP 不写"
|
||||
},
|
||||
{
|
||||
"case": "tvshow",
|
||||
"tag": "studio",
|
||||
"reason": "剧集写播出平台(TMDB networks,本例 Apple TV+);MP 写出品公司 production_companies"
|
||||
},
|
||||
{
|
||||
"case": "s01e01",
|
||||
"tag": "uniqueid",
|
||||
"reason": "本地不存 TMDB 集 id,集 NFO 不写 uniqueid,媒体服务器按季号集号匹配"
|
||||
},
|
||||
{
|
||||
"case": "s01e01",
|
||||
"tag": "tmdbid",
|
||||
"reason": "同上"
|
||||
},
|
||||
{
|
||||
"case": "s01e01",
|
||||
"tag": "runtime",
|
||||
"reason": "我们额外写集时长,MP 不写"
|
||||
},
|
||||
{
|
||||
"case": "s01e02",
|
||||
"tag": "uniqueid",
|
||||
"reason": "同 s01e01"
|
||||
},
|
||||
{
|
||||
"case": "s01e02",
|
||||
"tag": "tmdbid",
|
||||
"reason": "同 s01e01"
|
||||
},
|
||||
{
|
||||
"case": "s01e02",
|
||||
"tag": "runtime",
|
||||
"reason": "同 s01e01"
|
||||
}
|
||||
],
|
||||
"images": [
|
||||
{
|
||||
"role": "movie poster / fanart",
|
||||
"mp": "poster.jpg、backdrop.jpg、fanart.jpg(作品目录)",
|
||||
"ours": "poster.jpg、fanart.jpg",
|
||||
"reason": "backdrop.jpg 是 fanart.jpg 的副本,Emby / Jellyfin / Kodi 都认 fanart.jpg,不写重复文件"
|
||||
},
|
||||
{
|
||||
"role": "episode thumb",
|
||||
"mp": "<视频名>.jpg",
|
||||
"ours": "<视频名>-thumb.jpg",
|
||||
"reason": "Kodi 约定 -thumb 后缀;Jellyfin / Emby 同样识别,且不会和同名海报规则冲突"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fetchRichDetail, fetchRichSeason } from "@umt/adapters";
|
||||
import { episodeNfo, planScrapeTargets, seasonNfo, seasonPosterName, workNfo } from "@umt/domain";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { nfoEpisode, nfoSeason, nfoWork } from "../../src/modules/acquisition-organize/services/scrape-input.ts";
|
||||
import { richEntityFields } from "../../src/modules/sources-identity/service/metadata-rich.ts";
|
||||
|
||||
// SCR-1 NFO 差分:同一份 TMDB 原始响应(tmdb-raw.json),MP 走 set_tmdb_info + TmdbScraper(gen-scrape-mp.py → scrape-mp.json),
|
||||
// 我们走 fetchRichDetail / fetchRichSeason → 字段投影(metadata)→ scrape-input → domain NFO 生成器。
|
||||
// 双方去掉空元素后逐行比较,已登记的差异(nfo-deviations.json)按元素排除并要求确实不同。
|
||||
|
||||
const read = (f: string) => JSON.parse(readFileSync(new URL(f, import.meta.url), "utf8"));
|
||||
const raw = read("./tmdb-raw.json") as Record<string, unknown>;
|
||||
const mp = read("./scrape-mp.json") as Record<string, string>;
|
||||
const devs = read("./nfo-deviations.json").deviations as { case: string; tag: string }[];
|
||||
|
||||
const LANGS = ["zh-CN", "zh-SG", "zh-TW", "zh-HK", "en-US"];
|
||||
const client = {
|
||||
async getJson<T>(path: string): Promise<T> {
|
||||
const r = raw[path];
|
||||
if (!r) throw new Error(`fixture 缺 ${path}`);
|
||||
return structuredClone(r) as T;
|
||||
},
|
||||
};
|
||||
const final = (fields: Record<string, { value: unknown }>) =>
|
||||
Object.fromEntries(Object.entries(fields).map(([k, v]) => [k, { value: v.value }]));
|
||||
|
||||
async function ours(): Promise<Record<string, string>> {
|
||||
const out: Record<string, string> = {};
|
||||
const movie = await fetchRichDetail(client, "movie", 157336, LANGS);
|
||||
out.movie = workNfo(
|
||||
nfoWork(
|
||||
{
|
||||
kind: "movie",
|
||||
title: movie.title,
|
||||
original_title: movie.originalTitle,
|
||||
year: movie.year,
|
||||
metadata: final(richEntityFields(movie) as never),
|
||||
},
|
||||
{ tmdb: "157336" },
|
||||
),
|
||||
);
|
||||
const tv = await fetchRichDetail(client, "tv", 95396, LANGS);
|
||||
out.tvshow = workNfo(
|
||||
nfoWork(
|
||||
{
|
||||
kind: "series",
|
||||
title: tv.title,
|
||||
original_title: tv.originalTitle,
|
||||
year: tv.year,
|
||||
metadata: final(richEntityFields(tv) as never),
|
||||
},
|
||||
{ tmdb: "95396" },
|
||||
),
|
||||
);
|
||||
const sd = await fetchRichSeason(client, 95396, 1, LANGS, { now: new Date("2026-01-01") });
|
||||
out.season1 = seasonNfo(
|
||||
nfoSeason(1, {
|
||||
season_number: 1,
|
||||
episode_number: null,
|
||||
title: sd.name,
|
||||
air_date: sd.airDate,
|
||||
metadata: { name: { value: sd.name }, overview: { value: sd.overview }, airDate: { value: sd.airDate } },
|
||||
}),
|
||||
);
|
||||
for (const e of sd.episodes) {
|
||||
out[`s01e${String(e.episodeNumber).padStart(2, "0")}`] = episodeNfo(
|
||||
nfoEpisode(1, e.episodeNumber, {
|
||||
season_number: 1,
|
||||
episode_number: e.episodeNumber,
|
||||
title: e.name,
|
||||
air_date: e.airDate,
|
||||
metadata: {
|
||||
name: { value: e.name },
|
||||
overview: { value: e.overview },
|
||||
airDate: { value: e.airDate },
|
||||
runtime: { value: e.runtime },
|
||||
voteAverage: { value: e.voteAverage },
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 去空元素(MP 缺值写 <x/>、空 CDATA、评分 0) */
|
||||
const normalize = (xml: string) =>
|
||||
xml
|
||||
.split("\n")
|
||||
.filter((l) => l.trim() !== "")
|
||||
.filter((l) => !/^\s*<[\w]+(\s[^>]*)?\/>$/.test(l))
|
||||
.filter((l) => !/<!\[CDATA\[\]\]>/.test(l))
|
||||
.filter((l) => !/^\s*<rating>0<\/rating>$/.test(l));
|
||||
const tagOf = (l: string) => /^ {2}<(\w+)/.exec(l)?.[1] ?? null;
|
||||
|
||||
describe("NFO 与 MP 差分", async () => {
|
||||
const mine = await ours();
|
||||
const cases = ["movie", "tvshow", "season1", "s01e01", "s01e02"];
|
||||
|
||||
it.each(cases)("%s:结构与取值一致(已登记差异除外)", (c) => {
|
||||
const skip = new Set(devs.filter((d) => d.case === c).map((d) => d.tag));
|
||||
const a = normalize(mp[c]!).filter((l) => !skip.has(tagOf(l) ?? ""));
|
||||
const b = normalize(mine[c]!).filter((l) => !skip.has(tagOf(l) ?? ""));
|
||||
expect(b).toEqual(a);
|
||||
for (const tag of skip) {
|
||||
const ma = normalize(mp[c]!).filter((l) => tagOf(l) === tag);
|
||||
const mb = normalize(mine[c]!).filter((l) => tagOf(l) === tag);
|
||||
expect(mb, `登记的差异 ${c}.${tag} 已消失,删掉登记`).not.toEqual(ma);
|
||||
}
|
||||
});
|
||||
|
||||
it("编码与声明:UTF-8、两空格缩进、末尾换行,和 MP 一致", () => {
|
||||
for (const c of cases) {
|
||||
expect(mine[c]!.split("\n")[0]).toBe(mp[c]!.split("\n")[0]);
|
||||
expect(mine[c]!.endsWith("\n")).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("季海报命名与 MP get_season_poster 一致", () => {
|
||||
expect(seasonPosterName(1)).toBe(mp.seasonPosterName);
|
||||
expect(seasonPosterName(0)).toBe(mp.seasonSpecialsPosterName);
|
||||
});
|
||||
|
||||
it("文件命名:对照 MP scraping.py 的目标路径(差异见 nfo-deviations.json images)", () => {
|
||||
const t = planScrapeTargets({
|
||||
kind: "tv",
|
||||
workDir: "人生切割术 (2022)",
|
||||
videos: [{ path: "人生切割术 (2022)/Season 1/人生切割术 - S01E01 - 第 1 集.mkv", season: 1, episode: 1 }],
|
||||
images: {
|
||||
posterPath: "/p.jpg",
|
||||
backdropPath: "/b.jpg",
|
||||
seasonPosters: new Map([[1, "/s.jpg"]]),
|
||||
stills: new Map([["1:1", "/e.jpg"]]),
|
||||
},
|
||||
options: { images: true, episodeThumbs: true },
|
||||
});
|
||||
// MP:tvshow.nfo(TV)、season.nfo(SEASON)、<stem>.nfo(EPISODE);季海报根目录 seasonXX-poster + 季目录 poster
|
||||
expect(t.map((x) => x.path)).toEqual([
|
||||
"人生切割术 (2022)/tvshow.nfo",
|
||||
"人生切割术 (2022)/poster.jpg",
|
||||
"人生切割术 (2022)/fanart.jpg",
|
||||
"人生切割术 (2022)/Season 1/season.nfo",
|
||||
"人生切割术 (2022)/season01-poster.jpg",
|
||||
"人生切割术 (2022)/Season 1/poster.jpg",
|
||||
"人生切割术 (2022)/Season 1/人生切割术 - S01E01 - 第 1 集.nfo",
|
||||
"人生切割术 (2022)/Season 1/人生切割术 - S01E01 - 第 1 集-thumb.jpg",
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"movie": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<movie>\n <tmdbid>157336</tmdbid>\n <uniqueid type=\"tmdb\" default=\"false\">157336</uniqueid>\n <imdbid>tt0816692</imdbid>\n <uniqueid type=\"imdb\" default=\"true\">tt0816692</uniqueid>\n <plot><![CDATA[在不远的未来,随着地球自然环境的恶化,人类面临着无法生存的威胁。一群宇航员穿越虫洞,为人类寻找新家园。]]></plot>\n <outline><![CDATA[在不远的未来,随着地球自然环境的恶化,人类面临着无法生存的威胁。一群宇航员穿越虫洞,为人类寻找新家园。]]></outline>\n <director tmdbid=\"525\">克里斯托弗·诺兰</director>\n <actor>\n <name>马修·麦康纳</name>\n <type>Actor</type>\n <role>Cooper</role>\n <tmdbid>10297</tmdbid>\n <thumb>https://image.tmdb.org/t/p/original/lCySuYjhXix3FzQdS4oceDDrXKI.jpg</thumb>\n <profile>https://www.themoviedb.org/person/10297</profile>\n </actor>\n <actor>\n <name>安妮·海瑟薇</name>\n <type>Actor</type>\n <role>Brand</role>\n <tmdbid>1813</tmdbid>\n <thumb>https://image.tmdb.org/t/p/original/s6tflSD20MGz04ZR2R1lZvhmC4Y.jpg</thumb>\n <profile>https://www.themoviedb.org/person/1813</profile>\n </actor>\n <actor>\n <name>杰西卡·查斯坦</name>\n <type>Actor</type>\n <role>Murph</role>\n <tmdbid>83002</tmdbid>\n <profile>https://www.themoviedb.org/person/83002</profile>\n </actor>\n <genre>冒险</genre>\n <genre>剧情</genre>\n <genre>科幻</genre>\n <rating>8.5</rating>\n <studio>Legendary Pictures</studio>\n <country>英国</country>\n <country>美国</country>\n <runtime>169</runtime>\n <title>星际穿越</title>\n <originaltitle>Interstellar</originaltitle>\n <premiered>2014-11-05</premiered>\n <year>2014</year>\n</movie>\n",
|
||||
"tvshow": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<tvshow>\n <tmdbid>95396</tmdbid>\n <uniqueid type=\"tmdb\" default=\"false\">95396</uniqueid>\n <tvdbid>371980</tvdbid>\n <uniqueid type=\"tvdb\">371980</uniqueid>\n <imdbid>tt11280740</imdbid>\n <uniqueid type=\"imdb\" default=\"true\">tt11280740</uniqueid>\n <plot><![CDATA[马克领导着一个办公室团队,他们的记忆被手术分割,工作与生活从此一分为二。]]></plot>\n <outline><![CDATA[马克领导着一个办公室团队,他们的记忆被手术分割,工作与生活从此一分为二。]]></outline>\n <actor>\n <name>亚当·斯科特</name>\n <type>Actor</type>\n <role>Mark Scout</role>\n <tmdbid>4495</tmdbid>\n <thumb>https://image.tmdb.org/t/p/original/9ynbGSZ7O7stcCYx1ElmFhUgCKj.jpg</thumb>\n <profile>https://www.themoviedb.org/person/4495</profile>\n </actor>\n <actor>\n <name>布丽特·洛薇尔</name>\n <type>Actor</type>\n <role>Helly R.</role>\n <tmdbid>1319469</tmdbid>\n <profile>https://www.themoviedb.org/person/1319469</profile>\n </actor>\n <genre>剧情</genre>\n <genre>悬疑</genre>\n <genre>Sci-Fi & Fantasy</genre>\n <rating>8.4</rating>\n <studio>Red Hour Productions</studio>\n <country>美国</country>\n <title>人生切割术</title>\n <originaltitle>Severance</originaltitle>\n <premiered>2022-02-17</premiered>\n <year>2022</year>\n <season>-1</season>\n <episode>-1</episode>\n</tvshow>\n",
|
||||
"season1": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<season>\n <plot><![CDATA[在卢蒙工业公司,员工们接受了一种名为“分割”的手术 & 工作记忆与生活记忆被彻底隔开。]]></plot>\n <outline><![CDATA[在卢蒙工业公司,员工们接受了一种名为“分割”的手术 & 工作记忆与生活记忆被彻底隔开。]]></outline>\n <title>第 1 季</title>\n <premiered>2022-02-17</premiered>\n <releasedate>2022-02-17</releasedate>\n <year>2022</year>\n <seasonnumber>1</seasonnumber>\n</season>\n",
|
||||
"s01e01": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<episodedetails>\n <uniqueid type=\"tmdb\" default=\"true\">1830836</uniqueid>\n <tmdbid>1830836</tmdbid>\n <title>有关地狱的好消息</title>\n <plot><![CDATA[马克升职成为宏观数据精炼部的主管,并迎来一位新员工。]]></plot>\n <outline><![CDATA[马克升职成为宏观数据精炼部的主管,并迎来一位新员工。]]></outline>\n <aired>2022-02-17</aired>\n <year>2022</year>\n <season>1</season>\n <episode>1</episode>\n <rating>8.2</rating>\n</episodedetails>\n",
|
||||
"s01e02": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<episodedetails>\n <uniqueid type=\"tmdb\" default=\"true\">3435543</uniqueid>\n <tmdbid>3435543</tmdbid>\n <title>半循环</title>\n <plot><![CDATA[马克带海莉参观办公室。]]></plot>\n <outline><![CDATA[马克带海莉参观办公室。]]></outline>\n <aired>2022-02-18</aired>\n <year>2022</year>\n <season>1</season>\n <episode>2</episode>\n <rating>8.0</rating>\n</episodedetails>\n",
|
||||
"seasonPosterName": "season01-poster.jpg",
|
||||
"seasonSpecialsPosterName": "season-specials-poster.jpg"
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
{
|
||||
"_note": "SCR-1 差分用 TMDB 原始响应(zh-CN,形态照 TMDB v3;数值只保证合理,不作权威数据)。MP 与我们都从这里出发生成 NFO。",
|
||||
"/movie/157336": {
|
||||
"id": 157336,
|
||||
"title": "星际穿越",
|
||||
"original_title": "Interstellar",
|
||||
"original_language": "en",
|
||||
"origin_country": ["US"],
|
||||
"release_date": "2014-11-05",
|
||||
"overview": "在不远的未来,随着地球自然环境的恶化,人类面临着无法生存的威胁。一群宇航员穿越虫洞,为人类寻找新家园。",
|
||||
"tagline": "我们注定要离开地球",
|
||||
"runtime": 169,
|
||||
"vote_average": 8.464,
|
||||
"vote_count": 37000,
|
||||
"status": "Released",
|
||||
"poster_path": "/nCbkOyOMTEwlEV0LtCOvCnwEONA.jpg",
|
||||
"backdrop_path": "/xJHokMbljvjADYdit5fK5VQsXEG.jpg",
|
||||
"genres": [{ "id": 12, "name": "冒险" }, { "id": 18, "name": "剧情" }, { "id": 878, "name": "科幻" }],
|
||||
"production_countries": [{ "iso_3166_1": "GB", "name": "英国" }, { "iso_3166_1": "US", "name": "美国" }],
|
||||
"production_companies": [{ "id": 923, "name": "Legendary Pictures" }],
|
||||
"external_ids": { "imdb_id": "tt0816692", "wikidata_id": "Q13417189" },
|
||||
"credits": {
|
||||
"cast": [
|
||||
{
|
||||
"id": 10297,
|
||||
"name": "马修·麦康纳",
|
||||
"original_name": "Matthew McConaughey",
|
||||
"character": "Cooper",
|
||||
"known_for_department": "Acting",
|
||||
"order": 0,
|
||||
"profile_path": "/lCySuYjhXix3FzQdS4oceDDrXKI.jpg"
|
||||
},
|
||||
{
|
||||
"id": 1813,
|
||||
"name": "安妮·海瑟薇",
|
||||
"original_name": "Anne Hathaway",
|
||||
"character": "Brand",
|
||||
"known_for_department": "Acting",
|
||||
"order": 1,
|
||||
"profile_path": "/s6tflSD20MGz04ZR2R1lZvhmC4Y.jpg"
|
||||
},
|
||||
{
|
||||
"id": 83002,
|
||||
"name": "杰西卡·查斯坦",
|
||||
"original_name": "Jessica Chastain",
|
||||
"character": "Murph",
|
||||
"known_for_department": "Acting",
|
||||
"order": 2,
|
||||
"profile_path": null
|
||||
}
|
||||
],
|
||||
"crew": [
|
||||
{
|
||||
"id": 525,
|
||||
"name": "克里斯托弗·诺兰",
|
||||
"original_name": "Christopher Nolan",
|
||||
"job": "Director",
|
||||
"department": "Directing",
|
||||
"known_for_department": "Directing",
|
||||
"profile_path": "/xuAIuYSmsUzKlUMBFGVZaWsY3DZ.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
"translations": {
|
||||
"translations": [
|
||||
{
|
||||
"iso_639_1": "zh",
|
||||
"iso_3166_1": "CN",
|
||||
"data": {
|
||||
"title": "星际穿越",
|
||||
"overview": "在不远的未来,随着地球自然环境的恶化,人类面临着无法生存的威胁。一群宇航员穿越虫洞,为人类寻找新家园。",
|
||||
"tagline": "我们注定要离开地球"
|
||||
}
|
||||
},
|
||||
{
|
||||
"iso_639_1": "en",
|
||||
"iso_3166_1": "US",
|
||||
"data": {
|
||||
"title": "Interstellar",
|
||||
"overview": "The adventures of a group of explorers.",
|
||||
"tagline": "Mankind was born on Earth. It was never meant to die here."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"alternative_titles": { "titles": [{ "iso_3166_1": "CN", "title": "星际启示录", "type": "" }] }
|
||||
},
|
||||
"/tv/95396": {
|
||||
"id": 95396,
|
||||
"name": "人生切割术",
|
||||
"original_name": "Severance",
|
||||
"original_language": "en",
|
||||
"origin_country": ["US"],
|
||||
"first_air_date": "2022-02-17",
|
||||
"last_air_date": "2025-03-21",
|
||||
"overview": "马克领导着一个办公室团队,他们的记忆被手术分割,工作与生活从此一分为二。",
|
||||
"tagline": "",
|
||||
"vote_average": 8.395,
|
||||
"vote_count": 2400,
|
||||
"status": "Returning Series",
|
||||
"episode_run_time": [],
|
||||
"number_of_seasons": 2,
|
||||
"number_of_episodes": 19,
|
||||
"poster_path": "/pPHpeI2X1qEd1CS1SeyrdhZ4qnT.jpg",
|
||||
"backdrop_path": "/hS8cOSG9m8H9dGaIxkRYZ7rRl8F.jpg",
|
||||
"genres": [
|
||||
{ "id": 18, "name": "剧情" },
|
||||
{ "id": 9648, "name": "悬疑" },
|
||||
{ "id": 10765, "name": "Sci-Fi & Fantasy" }
|
||||
],
|
||||
"networks": [{ "id": 2552, "name": "Apple TV+" }],
|
||||
"production_countries": [{ "iso_3166_1": "US", "name": "美国" }],
|
||||
"production_companies": [{ "id": 1, "name": "Red Hour Productions" }],
|
||||
"created_by": [{ "id": 3442409, "name": "丹·埃里克森", "profile_path": null }],
|
||||
"external_ids": { "imdb_id": "tt11280740", "tvdb_id": 371980 },
|
||||
"credits": {
|
||||
"cast": [
|
||||
{
|
||||
"id": 4495,
|
||||
"name": "亚当·斯科特",
|
||||
"original_name": "Adam Scott",
|
||||
"character": "Mark Scout",
|
||||
"known_for_department": "Acting",
|
||||
"order": 0,
|
||||
"profile_path": "/9ynbGSZ7O7stcCYx1ElmFhUgCKj.jpg"
|
||||
},
|
||||
{
|
||||
"id": 1319469,
|
||||
"name": "布丽特·洛薇尔",
|
||||
"original_name": "Britt Lower",
|
||||
"character": "Helly R.",
|
||||
"known_for_department": "Acting",
|
||||
"order": 1,
|
||||
"profile_path": null
|
||||
}
|
||||
],
|
||||
"crew": []
|
||||
},
|
||||
"seasons": [
|
||||
{
|
||||
"season_number": 1,
|
||||
"episode_count": 9,
|
||||
"name": "第 1 季",
|
||||
"air_date": "2022-02-17",
|
||||
"overview": "",
|
||||
"poster_path": "/lFf6LLrQjYldcZItzOkGmMMigP7.jpg"
|
||||
},
|
||||
{
|
||||
"season_number": 2,
|
||||
"episode_count": 10,
|
||||
"name": "第 2 季",
|
||||
"air_date": "2025-01-16",
|
||||
"overview": "",
|
||||
"poster_path": "/5n9UHSJp9KcHSTnkBPkcQqgWUgL.jpg"
|
||||
}
|
||||
],
|
||||
"translations": {
|
||||
"translations": [
|
||||
{
|
||||
"iso_639_1": "zh",
|
||||
"iso_3166_1": "CN",
|
||||
"data": {
|
||||
"name": "人生切割术",
|
||||
"overview": "马克领导着一个办公室团队,他们的记忆被手术分割,工作与生活从此一分为二。",
|
||||
"tagline": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"iso_639_1": "en",
|
||||
"iso_3166_1": "US",
|
||||
"data": { "name": "Severance", "overview": "Mark leads a team of office workers.", "tagline": "" }
|
||||
}
|
||||
]
|
||||
},
|
||||
"alternative_titles": { "results": [{ "iso_3166_1": "TW", "title": "人生切割術", "type": "" }] }
|
||||
},
|
||||
"/tv/95396/season/1": {
|
||||
"id": 186498,
|
||||
"season_number": 1,
|
||||
"name": "第 1 季",
|
||||
"overview": "在卢蒙工业公司,员工们接受了一种名为“分割”的手术 & 工作记忆与生活记忆被彻底隔开。",
|
||||
"air_date": "2022-02-17",
|
||||
"poster_path": "/lFf6LLrQjYldcZItzOkGmMMigP7.jpg",
|
||||
"episodes": [
|
||||
{
|
||||
"id": 1830836,
|
||||
"episode_number": 1,
|
||||
"season_number": 1,
|
||||
"name": "有关地狱的好消息",
|
||||
"overview": "马克升职成为宏观数据精炼部的主管,并迎来一位新员工。",
|
||||
"air_date": "2022-02-17",
|
||||
"runtime": 57,
|
||||
"still_path": "/jM3lJqrr6uD17rBHCM7qc2rH4rM.jpg",
|
||||
"vote_average": 8.2,
|
||||
"vote_count": 171,
|
||||
"crew": [],
|
||||
"guest_stars": []
|
||||
},
|
||||
{
|
||||
"id": 3435543,
|
||||
"episode_number": 2,
|
||||
"season_number": 1,
|
||||
"name": "半循环",
|
||||
"overview": "马克带海莉参观办公室。",
|
||||
"air_date": "2022-02-18",
|
||||
"runtime": 53,
|
||||
"still_path": "/kg2cNi5r1hhvF5GWWyi3wYO7rqE.jpg",
|
||||
"vote_average": 8.0,
|
||||
"vote_count": 150,
|
||||
"crew": [],
|
||||
"guest_stars": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user