feat(集数映射): 作品季集结构字段(迁移 0075)、剧集组接口契约与按 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
5434ae8ee8
commit
3f6d8e410f
9 files changed
+271
-1
No files matched your search
@@ -51,4 +51,33 @@ export const episodeMappingApi = {
|
|||||||
response: em.EpisodeMappingPreview,
|
response: em.EpisodeMappingPreview,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
episodeGroups: {
|
||||||
|
list: ep({
|
||||||
|
method: "get",
|
||||||
|
path: "/media-entities/{id}/episode-groups",
|
||||||
|
scope: "read",
|
||||||
|
summary: "作品可选的季集结构:TMDB 剧集组列表与当前选择",
|
||||||
|
params: IdParam,
|
||||||
|
response: em.EpisodeGroupList,
|
||||||
|
}),
|
||||||
|
seasons: ep({
|
||||||
|
method: "get",
|
||||||
|
path: "/media-entities/{id}/episode-structure",
|
||||||
|
scope: "read",
|
||||||
|
summary: "某套季集结构(TMDB 默认季或剧集组)的季卡片与每集,带本地已入库情况(纯读)",
|
||||||
|
params: IdParam,
|
||||||
|
query: em.EpisodeStructureQuery,
|
||||||
|
response: em.EpisodeStructureSeasons,
|
||||||
|
}),
|
||||||
|
set: ep({
|
||||||
|
method: "put",
|
||||||
|
path: "/media-entities/{id}/episode-group",
|
||||||
|
scope: "organize.execute",
|
||||||
|
summary:
|
||||||
|
"切换作品的季集结构:按 TMDB 原季集原地迁移集单元(引用不动),重建季表,按作品重算已绑定资源并刷新元数据",
|
||||||
|
params: IdParam,
|
||||||
|
body: em.EpisodeGroupSetInput,
|
||||||
|
response: em.EpisodeGroupSetResult,
|
||||||
|
}),
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
@@ -25,6 +25,11 @@ export const MediaEntity = EntityBase.extend({
|
|||||||
lastMetadataRefreshAt: Timestamp.nullable(),
|
lastMetadataRefreshAt: Timestamp.nullable(),
|
||||||
/** 作品级命名模板覆盖 {movie?, tv?, variety?}(见 settings NamingOverride);null = 不覆盖。经 PUT /media-entities/{id}/naming-override 修改 */
|
/** 作品级命名模板覆盖 {movie?, tv?, variety?}(见 settings NamingOverride);null = 不覆盖。经 PUT /media-entities/{id}/naming-override 修改 */
|
||||||
namingOverride: JsonObject.nullable().optional(),
|
namingOverride: JsonObject.nullable().optional(),
|
||||||
|
/** 季集结构:TMDB 剧集组 id / 名称;null = TMDB 默认季。经 PUT /media-entities/{id}/episode-group 修改 */
|
||||||
|
episodeGroupId: z.string().nullable().optional(),
|
||||||
|
episodeGroupName: z.string().nullable().optional(),
|
||||||
|
episodeGroupSetAt: Timestamp.nullable().optional(),
|
||||||
|
episodeGroupSetBy: z.string().nullable().optional(),
|
||||||
});
|
});
|
||||||
export type MediaEntity = z.infer<typeof MediaEntity>;
|
export type MediaEntity = z.infer<typeof MediaEntity>;
|
||||||
export const MediaEntityCreate = MediaEntity.pick({
|
export const MediaEntityCreate = MediaEntity.pick({
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export const EpisodeResolutionMethod = z.enum([
|
|||||||
"absolute_in_season",
|
"absolute_in_season",
|
||||||
"absolute_cross_season",
|
"absolute_cross_season",
|
||||||
"air_date",
|
"air_date",
|
||||||
|
"tmdb_season",
|
||||||
"unverified",
|
"unverified",
|
||||||
"unresolved",
|
"unresolved",
|
||||||
"skipped",
|
"skipped",
|
||||||
@@ -197,6 +198,8 @@ export const EpisodeSeasonSummary = z.object({
|
|||||||
),
|
),
|
||||||
latestSeason: z.number().int().nullable(),
|
latestSeason: z.number().int().nullable(),
|
||||||
ended: z.boolean(),
|
ended: z.boolean(),
|
||||||
|
/** 季表来自哪套季集结构:null = TMDB 默认季;否则为作品选的剧集组 */
|
||||||
|
structure: z.object({ groupId: z.string(), name: z.string() }).nullable().optional(),
|
||||||
});
|
});
|
||||||
export type EpisodeSeasonSummary = z.infer<typeof EpisodeSeasonSummary>;
|
export type EpisodeSeasonSummary = z.infer<typeof EpisodeSeasonSummary>;
|
||||||
|
|
||||||
@@ -210,3 +213,119 @@ export const EpisodeMappingPreview = z.object({
|
|||||||
changed: z.number().int().min(0),
|
changed: z.number().int().min(0),
|
||||||
});
|
});
|
||||||
export type EpisodeMappingPreview = z.infer<typeof EpisodeMappingPreview>;
|
export type EpisodeMappingPreview = z.infer<typeof EpisodeMappingPreview>;
|
||||||
|
|
||||||
|
// ── 剧集组(季集结构):作品可选 TMDB 默认季或某个剧集组,季表与季 / 集单元随之生成 ──
|
||||||
|
|
||||||
|
export const EpisodeGroupSummary = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
/** TMDB type 1–7;未知为 null */
|
||||||
|
type: z.number().int().nullable(),
|
||||||
|
/** 类型的中文名(原始播出 / 绝对集号 / DVD / 数字发行 / 故事线 / 制作季 / 电视) */
|
||||||
|
typeLabel: z.string().nullable(),
|
||||||
|
description: z.string().nullable(),
|
||||||
|
/** 组(= 季)数 */
|
||||||
|
groupCount: z.number().int().min(0),
|
||||||
|
episodeCount: z.number().int().min(0),
|
||||||
|
network: z.string().nullable(),
|
||||||
|
});
|
||||||
|
export type EpisodeGroupSummary = z.infer<typeof EpisodeGroupSummary>;
|
||||||
|
|
||||||
|
/** 作品当前的季集结构;null = TMDB 默认季 */
|
||||||
|
export const EpisodeStructureCurrent = z.object({
|
||||||
|
groupId: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
setAt: z.string().nullable(),
|
||||||
|
/** `<actor_type>:<actor_id>` */
|
||||||
|
setBy: z.string().nullable(),
|
||||||
|
});
|
||||||
|
export type EpisodeStructureCurrent = z.infer<typeof EpisodeStructureCurrent>;
|
||||||
|
|
||||||
|
export const EpisodeGroupList = z.object({
|
||||||
|
/** 作品是否能选剧集组(剧集 / 综艺且有 TMDB 剧集身份) */
|
||||||
|
applicable: z.boolean(),
|
||||||
|
current: EpisodeStructureCurrent.nullable(),
|
||||||
|
groups: z.array(EpisodeGroupSummary),
|
||||||
|
/** 取列表失败的原因码(TMDB 未配置 / 不可用);成功为 null */
|
||||||
|
error: z.string().nullable(),
|
||||||
|
});
|
||||||
|
export type EpisodeGroupList = z.infer<typeof EpisodeGroupList>;
|
||||||
|
|
||||||
|
export const EpisodeStructureEpisode = z.object({
|
||||||
|
episodeNumber: z.number().int(),
|
||||||
|
name: z.string().nullable(),
|
||||||
|
airDate: z.string().nullable(),
|
||||||
|
/** TMDB 默认季集号(剧集组里才有意义;默认季时与自身相同) */
|
||||||
|
tmdbSeason: z.number().int(),
|
||||||
|
tmdbEpisode: z.number().int(),
|
||||||
|
/** 本地已入库(按 TMDB 原季集对照,换结构前也能看) */
|
||||||
|
inLibrary: z.boolean(),
|
||||||
|
});
|
||||||
|
export type EpisodeStructureEpisode = z.infer<typeof EpisodeStructureEpisode>;
|
||||||
|
|
||||||
|
export const EpisodeStructureSeason = z.object({
|
||||||
|
seasonNumber: z.number().int(),
|
||||||
|
name: z.string().nullable(),
|
||||||
|
episodeCount: z.number().int().min(0),
|
||||||
|
/** 首播日期 */
|
||||||
|
airDate: z.string().nullable(),
|
||||||
|
posterPath: z.string().nullable(),
|
||||||
|
/** 本地已入库集数 */
|
||||||
|
inLibrary: z.number().int().min(0),
|
||||||
|
/** 当前结构下对应的季单元(选季订阅用);不是当前结构或还没建为 null */
|
||||||
|
seasonUnitId: Id.nullable(),
|
||||||
|
episodes: z.array(EpisodeStructureEpisode),
|
||||||
|
});
|
||||||
|
export type EpisodeStructureSeason = z.infer<typeof EpisodeStructureSeason>;
|
||||||
|
|
||||||
|
/** 某套季集结构的季卡片(TMDB 默认季或某个剧集组),带本地已入库情况 */
|
||||||
|
export const EpisodeStructureSeasons = z.object({
|
||||||
|
groupId: z.string().nullable(),
|
||||||
|
name: z.string(),
|
||||||
|
/** 是不是作品当前的季集结构 */
|
||||||
|
current: z.boolean(),
|
||||||
|
seasons: z.array(EpisodeStructureSeason),
|
||||||
|
});
|
||||||
|
export type EpisodeStructureSeasons = z.infer<typeof EpisodeStructureSeasons>;
|
||||||
|
|
||||||
|
export const EpisodeStructureQuery = z.object({
|
||||||
|
/** 剧集组 id;不填 = TMDB 默认季 */
|
||||||
|
groupId: z.string().trim().min(1).max(64).optional(),
|
||||||
|
});
|
||||||
|
export type EpisodeStructureQuery = z.infer<typeof EpisodeStructureQuery>;
|
||||||
|
|
||||||
|
export const EpisodeGroupSetInput = z.object({
|
||||||
|
/** 剧集组 id;null = 改回 TMDB 默认季 */
|
||||||
|
groupId: z.string().trim().min(1).max(64).nullable(),
|
||||||
|
});
|
||||||
|
export type EpisodeGroupSetInput = z.infer<typeof EpisodeGroupSetInput>;
|
||||||
|
|
||||||
|
/** 切换时对不上的集单元(保留原号、标临时) */
|
||||||
|
export const EpisodeGroupUnmatchedUnit = z.object({
|
||||||
|
unitId: Id,
|
||||||
|
seasonNumber: z.number().int().nullable(),
|
||||||
|
episodeNumber: z.number().int().nullable(),
|
||||||
|
/** no_origin:求不出 TMDB 原季集;not_in_target:目标结构里没有这一集;numbering_locked:编号已人工锁定 */
|
||||||
|
reason: z.enum(["no_origin", "not_in_target", "numbering_locked"]),
|
||||||
|
/** 有资产覆盖 / 资源 / 下载等引用 */
|
||||||
|
referenced: z.boolean(),
|
||||||
|
});
|
||||||
|
export type EpisodeGroupUnmatchedUnit = z.infer<typeof EpisodeGroupUnmatchedUnit>;
|
||||||
|
|
||||||
|
export const EpisodeGroupSetResult = z.object({
|
||||||
|
changed: z.boolean(),
|
||||||
|
current: EpisodeStructureCurrent.nullable(),
|
||||||
|
/** 改了季集号的集单元数 */
|
||||||
|
moved: z.number().int().min(0),
|
||||||
|
/** 季集号不变的集单元数 */
|
||||||
|
kept: z.number().int().min(0),
|
||||||
|
unmatched: z.array(EpisodeGroupUnmatchedUnit),
|
||||||
|
/** 新结构的季与集数 */
|
||||||
|
seasons: z.array(z.object({ seasonNumber: z.number().int(), episodeCount: z.number().int().min(0) })),
|
||||||
|
/** 不在新结构里、标为临时的季单元的季号 */
|
||||||
|
orphanSeasons: z.array(z.number().int()),
|
||||||
|
reresolve: EpisodeReresolveResult,
|
||||||
|
/** 切换后元数据刷新结果(refreshed / skipped / failed) */
|
||||||
|
refresh: z.string(),
|
||||||
|
});
|
||||||
|
export type EpisodeGroupSetResult = z.infer<typeof EpisodeGroupSetResult>;
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import type { Kysely } from "kysely";
|
||||||
|
import { execDdl } from "../src/migration-helpers.ts";
|
||||||
|
|
||||||
|
// 作品的季集结构(剧集组):null = TMDB 默认季;否则为 TMDB 剧集组 id(/tv/episode_group/{id}),季表与季 / 集单元按组生成。
|
||||||
|
// 名称冗余存一份(界面显示不用再查 TMDB);选择时间与操作者(`<actor_type>:<actor_id>`)供审计与界面「谁改的」。
|
||||||
|
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: 迁移不依赖应用表类型
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await execDdl(
|
||||||
|
db,
|
||||||
|
`
|
||||||
|
alter table media_entity add column episode_group_id text;
|
||||||
|
alter table media_entity add column episode_group_name text;
|
||||||
|
alter table media_entity add column episode_group_set_at timestamptz;
|
||||||
|
alter table media_entity add column episode_group_set_by text;
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: 同上
|
||||||
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await execDdl(
|
||||||
|
db,
|
||||||
|
`
|
||||||
|
alter table media_entity drop column if exists episode_group_set_by;
|
||||||
|
alter table media_entity drop column if exists episode_group_set_at;
|
||||||
|
alter table media_entity drop column if exists episode_group_name;
|
||||||
|
alter table media_entity drop column if exists episode_group_id;
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -952,6 +952,10 @@ export interface MediaEntityTable {
|
|||||||
created_at: GeneratedTimestamptz;
|
created_at: GeneratedTimestamptz;
|
||||||
updated_at: GeneratedTimestamptz;
|
updated_at: GeneratedTimestamptz;
|
||||||
naming_override: Json | null;
|
naming_override: Json | null;
|
||||||
|
episode_group_id: string | null;
|
||||||
|
episode_group_name: string | null;
|
||||||
|
episode_group_set_at: Timestamptz | null;
|
||||||
|
episode_group_set_by: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MediaServerInstanceTable {
|
export interface MediaServerInstanceTable {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { cumulativeSeasons, seasonOf } from "./table.ts";
|
|||||||
import type { CanonicalEpisodes, SeasonTable } from "./types.ts";
|
import type { CanonicalEpisodes, SeasonTable } from "./types.ts";
|
||||||
import { validateEpisodes } from "./validate.ts";
|
import { validateEpisodes } from "./validate.ts";
|
||||||
|
|
||||||
export type ConversionMethod = "absolute_in_season" | "absolute_cross_season" | "air_date";
|
export type ConversionMethod = "absolute_in_season" | "absolute_cross_season" | "air_date" | "tmdb_season";
|
||||||
|
|
||||||
export interface ConversionCandidate {
|
export interface ConversionCandidate {
|
||||||
method: ConversionMethod;
|
method: ConversionMethod;
|
||||||
@@ -88,6 +88,33 @@ export function airDateCandidates(airDate: string, table: SeasonTable, season: n
|
|||||||
return hits.map((canonical) => ({ method: "air_date", canonical, evidence: `air_date:${iso}` }));
|
return hits.map((canonical) => ({ method: "air_date", canonical, evidence: `air_date:${iso}` }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 剧集组结构下按 TMDB 默认季集换算:把发布者的季集当作 TMDB 默认季集,经季表的 origins 对照到组内季集。
|
||||||
|
* 区间内每一集都要能对上,且落在同一季、组内集号连续,否则不成立。季 0 不换。
|
||||||
|
*/
|
||||||
|
export function tmdbSeasonCandidate(
|
||||||
|
season: number,
|
||||||
|
episodes: EpisodeRange,
|
||||||
|
table: SeasonTable,
|
||||||
|
): ConversionCandidate | null {
|
||||||
|
const origins = table.origins;
|
||||||
|
if (!origins?.length || season < 1 || episodes.end - episodes.start > 200) return null;
|
||||||
|
const hits: { season: number; episode: number }[] = [];
|
||||||
|
for (let e = episodes.start; e <= episodes.end; e++) {
|
||||||
|
const o = origins.find((x) => x.tmdbSeason === season && x.tmdbEpisode === e);
|
||||||
|
if (!o) return null;
|
||||||
|
hits.push(o);
|
||||||
|
}
|
||||||
|
const first = hits[0]!;
|
||||||
|
if (hits.some((h, i) => h.season !== first.season || h.episode !== first.episode + i)) return null;
|
||||||
|
const pad = (n: number) => String(n).padStart(2, "0");
|
||||||
|
return {
|
||||||
|
method: "tmdb_season",
|
||||||
|
canonical: { season: first.season, episodes: { start: first.episode, end: hits.at(-1)!.episode } },
|
||||||
|
evidence: `tmdb_season:S${pad(season)}E${pad(episodes.start)}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface ConversionOutcome {
|
export interface ConversionOutcome {
|
||||||
/** 去重后、校验为 pass 的候选 */
|
/** 去重后、校验为 pass 的候选 */
|
||||||
candidates: ConversionCandidate[];
|
candidates: ConversionCandidate[];
|
||||||
@@ -117,6 +144,10 @@ export function convertEpisodes(input: {
|
|||||||
if (cross.kind === "split") evidence.push(cross.evidence);
|
if (cross.kind === "split") evidence.push(cross.evidence);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (episodes && season !== null) {
|
||||||
|
const origin = tmdbSeasonCandidate(season, episodes, table);
|
||||||
|
if (origin) raw.push(origin);
|
||||||
|
}
|
||||||
if (input.variety && airDate) {
|
if (input.variety && airDate) {
|
||||||
raw.push(...airDateCandidates(airDate, table, seasonInferred ? null : season));
|
raw.push(...airDateCandidates(airDate, table, seasonInferred ? null : season));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export {
|
|||||||
convertEpisodes,
|
convertEpisodes,
|
||||||
crossSeasonCandidate,
|
crossSeasonCandidate,
|
||||||
inSeasonCandidates,
|
inSeasonCandidates,
|
||||||
|
tmdbSeasonCandidate,
|
||||||
} from "./convert.ts";
|
} from "./convert.ts";
|
||||||
export { CONVERSION_NEAR_DAYS, resolveEpisodes } from "./resolve.ts";
|
export { CONVERSION_NEAR_DAYS, resolveEpisodes } from "./resolve.ts";
|
||||||
export { applyRule, type RuleApplication, ruleSpecificity, selectRule } from "./rules.ts";
|
export { applyRule, type RuleApplication, ruleSpecificity, selectRule } from "./rules.ts";
|
||||||
|
|||||||
@@ -13,9 +13,22 @@ export interface SeasonInfo {
|
|||||||
episodes?: SeasonEpisodeInfo[];
|
episodes?: SeasonEpisodeInfo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 剧集组结构下一集的 TMDB 默认季集 → 组内季集 */
|
||||||
|
export interface SeasonOrigin {
|
||||||
|
tmdbSeason: number;
|
||||||
|
tmdbEpisode: number;
|
||||||
|
season: number;
|
||||||
|
episode: number;
|
||||||
|
}
|
||||||
|
|
||||||
/** 作品的季表(来自元数据,调用方组装)。季 0(特别篇)可以在表里,换算时忽略 */
|
/** 作品的季表(来自元数据,调用方组装)。季 0(特别篇)可以在表里,换算时忽略 */
|
||||||
export interface SeasonTable {
|
export interface SeasonTable {
|
||||||
seasons: SeasonInfo[];
|
seasons: SeasonInfo[];
|
||||||
|
/**
|
||||||
|
* 作品选了 TMDB 剧集组时:每集的 TMDB 默认季集与组内季集对照。发布者按 TMDB 默认季编号(S01E14)而作品按组分季时,
|
||||||
|
* 换算一步按它把原号换成组内季集(方式 tmdb_season)。默认季结构不给。
|
||||||
|
*/
|
||||||
|
origins?: SeasonOrigin[];
|
||||||
/** 最新季;null 时取表中最大季号 */
|
/** 最新季;null 时取表中最大季号 */
|
||||||
latestSeason: number | null;
|
latestSeason: number | null;
|
||||||
/** 作品已完结:完结后不再有「元数据滞后」放行 */
|
/** 作品已完结:完结后不再有「元数据滞后」放行 */
|
||||||
@@ -58,6 +71,7 @@ export type EpisodeResolutionMethod =
|
|||||||
| "absolute_in_season"
|
| "absolute_in_season"
|
||||||
| "absolute_cross_season"
|
| "absolute_cross_season"
|
||||||
| "air_date"
|
| "air_date"
|
||||||
|
| "tmdb_season"
|
||||||
| "unverified"
|
| "unverified"
|
||||||
| "unresolved"
|
| "unresolved"
|
||||||
| "skipped";
|
| "skipped";
|
||||||
|
|||||||
@@ -280,3 +280,39 @@ describe("resolveEpisodes 规则", () => {
|
|||||||
expect(r).toMatchObject({ method: "unresolved", ruleFailed: "x" });
|
expect(r).toMatchObject({ method: "unresolved", ruleFailed: "x" });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("resolveEpisodes 剧集组:按 TMDB 默认季集换算", () => {
|
||||||
|
// TMDB 默认 S1 共 25 集;剧集组拆成 S1(1–12)、S2(13–25)
|
||||||
|
const grouped: SeasonTable = {
|
||||||
|
...table([12, 13]),
|
||||||
|
origins: Array.from({ length: 25 }, (_, k) => ({
|
||||||
|
tmdbSeason: 1,
|
||||||
|
tmdbEpisode: k + 1,
|
||||||
|
season: k < 12 ? 1 : 2,
|
||||||
|
episode: k < 12 ? k + 1 : k - 11,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
it("S01E14(按 TMDB 默认季编号)→ 组内 S02E02", () => {
|
||||||
|
const r = run({ season: 1, episodes: ep(14) }, grouped);
|
||||||
|
expect(r).toMatchObject({ method: "tmdb_season", canonical: { season: 2, episodes: ep(2) } });
|
||||||
|
expect(r.evidence).toContain("tmdb_season:S01E14");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("「- 13」季号推定:跨季绝对集号与默认季对照同解,归第 2 季第 1 集", () => {
|
||||||
|
const r = run({ season: 1, seasonInferred: true, episodes: ep(13) }, grouped);
|
||||||
|
expect(r.canonical).toEqual({ season: 2, episodes: ep(1) });
|
||||||
|
expect(["absolute_cross_season", "tmdb_season"]).toContain(r.method);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("区间跨组不换;组内号本身对得上时原号优先", () => {
|
||||||
|
expect(run({ season: 1, episodes: ep(12, 13) }, grouped).method).toBe("unresolved");
|
||||||
|
expect(run({ season: 2, episodes: ep(3) }, grouped)).toMatchObject({ method: "raw" });
|
||||||
|
expect(run({ season: 1, episodes: ep(14, 15) }, grouped).canonical).toEqual({ season: 2, episodes: ep(2, 3) });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("没有 origins(默认季结构)时不出这种换算", () => {
|
||||||
|
const { origins: _o, ...plain } = grouped;
|
||||||
|
expect(run({ season: 1, episodes: ep(14) }, plain).method).toBe("unresolved");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user