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:
rootandClaude Opus 5.5 committed 2026-09-30 22:36:14 +08:00
1 parent 861f799615
commit f38eca419e
24 files changed
+1740 -15

No files matched your search

+51
View File
@@ -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 同样识别,且不会和同名海报规则冲突"
}
]
}
+149
View File
@@ -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",
]);
});
});
+9
View File
@@ -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 &amp; 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"
}
+215
View File
@@ -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": []
}
]
}
}