diff --git a/apps/server/src/modules/search/works.ts b/apps/server/src/modules/search/works.ts index df0974fb..6d9be215 100644 --- a/apps/server/src/modules/search/works.ts +++ b/apps/server/src/modules/search/works.ts @@ -204,8 +204,9 @@ const emptyInventory = (entityId: string): InventoryEntry => ({ /** * 搜资源的查询词(每个站点每个词查一次,最多 2 个,控制对站点的请求数): - * 原名(电影带年份,站点标题多为「原名 年份 …」)+ 译名(与原名归一化后不同才加)。 - * 剧集不带年份(各季年份不同)。 + * 国内站点按中文名收录、副标题也多写中文名,所以作品名(中文)在前,原名第二(与作品名归一化后不同才加)。 + * 拉丁文字的名字在电影上带年份(站点标题多为「原名 年份 …」),只带一个;剧集不带年份(各季年份不同)。 + * 作品名不含中日韩文字(没有中文译名)时原名在前。 */ export function searchTerms(w: { title: string; @@ -215,17 +216,23 @@ export function searchTerms(w: { }): string[] { const out: string[] = []; const seen = new Set(); - const add = (t: string | null, withYear: boolean) => { + const latin = (s: string | null) => !!s && /^[\p{Script=Latin}\d\s\p{P}\p{S}]+$/u.test(s); + const add = (t: string | null) => { const s = t?.trim(); if (!s) return; const n = normalizeTitle(s); if (!n || seen.has(n)) return; seen.add(n); - out.push(withYear && w.year ? `${s} ${w.year}` : s); + const withYear = w.movie && !!w.year && latin(s) && !out.some(latin); + out.push(withYear ? `${s} ${w.year}` : s); }; - const latin = (s: string | null) => !!s && /^[\p{Script=Latin}\d\s\p{P}\p{S}]+$/u.test(s); - add(w.originalTitle, w.movie && latin(w.originalTitle)); - add(w.title, w.movie && latin(w.title) && out.length === 0); + if (/[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u.test(w.title)) { + add(w.title); + add(w.originalTitle); + } else { + add(w.originalTitle); + add(w.title); + } return out.slice(0, 2); } diff --git a/apps/server/test/search/two-stage-search.test.ts b/apps/server/test/search/two-stage-search.test.ts index 3ff572d3..d65a92f3 100644 --- a/apps/server/test/search/two-stage-search.test.ts +++ b/apps/server/test/search/two-stage-search.test.ts @@ -32,21 +32,32 @@ const up = await pgAvailable(); type Loose = any; describe("searchTerms", () => { - it("电影:原名带年份 + 译名;同名只留一个;剧集不带年份", () => { + it("中文作品名在前、原名第二;拉丁名在电影上带年份;同名只留一个;剧集不带年份", () => { expect(searchTerms({ title: "星际穿越", originalTitle: "Interstellar", year: 2014, movie: true })).toEqual([ - "Interstellar 2014", "星际穿越", + "Interstellar 2014", ]); expect(searchTerms({ title: "流浪地球2", originalTitle: "流浪地球2", year: 2023, movie: true })).toEqual([ "流浪地球2", ]); expect(searchTerms({ title: "熊家餐馆", originalTitle: "The Bear", year: 2022, movie: false })).toEqual([ - "The Bear", "熊家餐馆", + "The Bear", ]); expect(searchTerms({ title: "Oppenheimer", originalTitle: null, year: 2023, movie: true })).toEqual([ "Oppenheimer 2023", ]); + // 日文原名:中文名在前,原名第二 + expect( + searchTerms({ title: "药屋少女的呢喃", originalTitle: "薬屋のひとりごと", year: 2023, movie: false }), + ).toEqual(["药屋少女的呢喃", "薬屋のひとりごと"]); + // 繁简只差字形的算同一个 + expect(searchTerms({ title: "无间道", originalTitle: "無間道", year: 2002, movie: true })).toEqual(["无间道"]); + // 作品名没有中文译名(仍是英文)时原名在前 + expect( + searchTerms({ title: "The Apothecary Diaries", originalTitle: "薬屋のひとりごと", year: 2023, movie: false }), + ).toEqual(["薬屋のひとりごと", "The Apothecary Diaries"]); + expect(searchTerms({ title: " ", originalTitle: null, year: null, movie: false })).toEqual([]); }); }); @@ -287,18 +298,18 @@ describe.skipIf(!up)("两阶段搜索(SEARCH-2)", () => { await db.deleteFrom("settings").where("key", "=", "search").execute(); }); - it("按作品搜资源:服务端用原名 + 年份 / 译名生成查询词,每站每词一次", async () => { + it("按作品搜资源:服务端用中文名 / 原名 + 年份生成查询词,每站每词一次", async () => { const before = counts(); const r = await req(`/search/releases?tmdb=movie:693134&sites=${sites.a}`); expect(r.status).toBe(200); - expect(r.body.data.queries).toEqual(["Dune: Part Two 2024", "沙丘2"]); - expect(r.body.data.q).toBe("Dune: Part Two 2024"); + expect(r.body.data.queries).toEqual(["沙丘2", "Dune: Part Two 2024"]); + expect(r.body.data.q).toBe("沙丘2"); const sent = searches(a).slice(before[0]); expect(sent).toHaveLength(2); expect(r.body.data.sites[0]).toMatchObject({ siteId: sites.a, status: "ok" }); const byEntity = await req(`/search/releases?entityId=${dune}&sites=${sites.a}`); - expect(byEntity.body.data.queries).toEqual(["Dune: Part Two 2024", "沙丘2"]); + expect(byEntity.body.data.queries).toEqual(["沙丘2", "Dune: Part Two 2024"]); expect(byEntity.body.data.cached).toBe(true); expect((await req(`/search/releases?sites=${sites.a}`)).status).toBe(422); @@ -320,7 +331,7 @@ describe.skipIf(!up)("两阶段搜索(SEARCH-2)", () => { inventory: { state: "not_collected" }, mediaServers: null, seasons: [], - queries: ["Oppenheimer 2023", "奥本海默"], + queries: ["奥本海默", "Oppenheimer 2023"], externalIds: { imdb: "tt15398776" }, }); @@ -338,7 +349,7 @@ describe.skipIf(!up)("两阶段搜索(SEARCH-2)", () => { [2, 10, null], [3, 10, null], ]); - expect(tv.body.data.queries).toEqual(["The Bear", "熊家餐馆"]); + expect(tv.body.data.queries).toEqual(["熊家餐馆", "The Bear"]); expect((await req(`/media/tmdb/movie/999999999`)).status).toBe(404); expect((await req(`/media/tmdb/person/1`)).status).toBe(422);