From 8915e8b5f88a06d6379ba01ce974c7e29d19b5ff Mon Sep 17 00:00:00 2001 From: kyeunga25 <49793173+kyeunga25@users.noreply.github.com> Date: Mon, 7 Sep 2026 21:00:38 +0800 Subject: [PATCH] (add): preserve THE ORIGIN TV theme editions --- CHANGELOG.md | 6 + README.md | 6 +- docs/DATA_PROVENANCE.md | 4 +- docs/PROJECT_PLAN.md | 5 +- docs/QA_V1_42_2019_SPRING_ORIGIN.md | 32 ++++ package-lock.json | 4 +- package.json | 2 +- src/data/curated-season-registry.ts | 2 +- src/data/curated-seeds/2019/spring.ts | 170 +++++++++++++++++- src/data/curated-theme-sources/2019/spring.ts | 22 +++ src/data/curated-theme-videos/2019/spring.ts | 78 ++++++++ tests/e2e/phase1.spec.ts | 61 +++++-- tests/unit/api-smoke.test.ts | 2 +- tests/unit/catalog-navigation.test.ts | 2 +- tests/unit/curated-2019-spring.test.ts | 72 +++++++- tests/unit/curated-catalog.test.ts | 10 +- tests/unit/curated-season-registry.test.ts | 2 +- .../curated-theme-source-registry.test.ts | 4 +- .../unit/curated-theme-video-registry.test.ts | 6 +- tests/unit/page-data.test.ts | 6 +- 20 files changed, 455 insertions(+), 41 deletions(-) create mode 100644 docs/QA_V1_42_2019_SPRING_ORIGIN.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b6b994..1975bce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable public product changes are recorded here. Release notes contain only product scope and reproducible validation; platform identifiers and private operational details are excluded. +## v1.42.0 — 2019 春季 THE ORIGIN 電視版 + +- 收錄《機動戦士ガンダム THE ORIGIN 前夜 赤い彗星》2019 年 13 話電視重編版,與早期 OVA 身份分開。 +- 新增 3 首 OP、4 首 ED 及 8 筆官方影片 metadata;保留原曲作者、新演唱與編曲、原始發行日期及第 12 話片尾畫面版本。 +- 春季共 29 套作品、76 筆 OP/ED;本季與夏季仍在補充,2025 秋季尚未收錄。 + ## [1.41.1] - 2026-09-07 ### Fixed diff --git a/README.md b/README.md index 836771f..59f8a58 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ Anisonary 是以 Astro + strict TypeScript 建立的動畫歌曲目錄,按季度與日本編輯播出日瀏覽作品、OP 與 ED。網站輸出為純靜態資產,正式交付使用 **Cloudflare Workers Static Assets**;它不是 Cloudflare Pages,也沒有 application Worker backend。下方同時保留 English technical notes。 -This repository contains the completed static product: season directory, anime detail pages, traceable OP/ED credits and links, per-song source ledgers, source-attributed media, local-only cross-season search, privacy-bounded offline reading, a GitHub correction flow, and deployment through Cloudflare Workers Static Assets. The default catalogue covers twenty-nine reviewed snapshots across 2019–2026, with 1,945 unique titles and 4,298 known OP/ED records. The 2019 spring snapshot contains twenty-eight TV titles, including television shorts; summer contains 38 TV titles and four web series. Both quarters are still being expanded, and 2025 fall is not yet included. Fictional Mock Data remains test-only. +This repository contains the completed static product: season directory, anime detail pages, traceable OP/ED credits and links, per-song source ledgers, source-attributed media, local-only cross-season search, privacy-bounded offline reading, a GitHub correction flow, and deployment through Cloudflare Workers Static Assets. The default catalogue covers twenty-nine reviewed snapshots across 2019–2026, with 1,946 unique titles and 4,305 known OP/ED records. The 2019 spring snapshot contains twenty-nine TV titles, including television shorts and a TV recut; summer contains 38 TV titles and four web series. Both quarters are still being expanded, and 2025 fall is not yet included. Fictional Mock Data remains test-only. -目前 source 版本:**v1.41.1**。作品頁、季度卡片、搜尋結果與頁面 metadata 共用不重複的名稱顯示;保留不同寫法的繁中名稱及 Romaji,原始名稱、歌曲與 API 資料不變。目錄共有 29 個季度、1,945 套作品與 4,298 筆 OP/ED;春季為 28 套作品、69 筆歌曲,春、夏季仍在補充,2025 秋季尚未收錄。 +目前 source 版本:**v1.42.0**。2019 春季新增《機動戦士ガンダム THE ORIGIN 前夜 赤い彗星》13 話電視重編版,收錄 3 首 OP、4 首 ED 與 8 筆官方影片 metadata;保留翻唱者、製作署名、原始發行日期及第 12 話片尾畫面版本。目錄共有 29 個季度、1,946 套作品與 4,305 筆 OP/ED;春季為 29 套作品、76 筆歌曲,春、夏季仍在補充,2025 秋季尚未收錄。 導覽延續固定五個入口與年代、年份、季度目錄;搜尋可按動畫、歌曲、創作者、年份、季度及 OP/ED 篩選,每頁顯示 12 套作品。搜尋完全在瀏覽器內進行,YouTube 只於明確操作後連線。網站與同源靜態 JSON API 共用已審閱資料,無 application backend 或 database binding。依賴安全 gate 繼續鎖定 `fast-uri` 3.1.6 並稽核開發依賴。 @@ -12,7 +12,7 @@ Production build 會從最終 HTML 自動產生 hash-based Content Security Poli | 可用性 / Availability | 成熟度 / Maturity | 證據 / Evidence | | -------------------------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 公開靜態目錄 / Public static catalogue | Source 版本為 `v1.41.1`;正式發布狀態以對應 Release 驗收摘要為準 | [入口網站 / Live](https://anisonary.k-y.cc) · [版本與驗收 / Releases](https://github.com/kyeunga25/anisonary/releases) · [資料來源 / Sources](docs/DATA_SOURCES.md) · [安全政策 / Security](SECURITY.md) · [授權 / Licence](LICENSING.md) · [版權 / Copyright](COPYRIGHT.md) | +| 公開靜態目錄 / Public static catalogue | Source 版本為 `v1.42.0`;正式發布狀態以對應 Release 驗收摘要為準 | [入口網站 / Live](https://anisonary.k-y.cc) · [版本與驗收 / Releases](https://github.com/kyeunga25/anisonary/releases) · [資料來源 / Sources](docs/DATA_SOURCES.md) · [安全政策 / Security](SECURITY.md) · [授權 / Licence](LICENSING.md) · [版權 / Copyright](COPYRIGHT.md) | ## 技術棧|Technology stack diff --git a/docs/DATA_PROVENANCE.md b/docs/DATA_PROVENANCE.md index 0b723e7..af4373e 100644 --- a/docs/DATA_PROVENANCE.md +++ b/docs/DATA_PROVENANCE.md @@ -31,9 +31,9 @@ API、資料庫、商標或媒體重用權。完整授權邊界見 `first_party` 表示動畫製作/發行方、唱片公司、藝人官方渠道、官方影片或正式播出機構的公開頁面。`cross_check` 表示用於發現缺口及交叉比對的公開索引。兩種角色都是來源分類,不代表數值信心或內容評分。 -2019 春季目前整理 28 套 TV 作品(含電視短篇),夏季整理 38 套 TV 作品及 4 套網絡連載;兩季的其他作品、跨季延續與特殊歌曲仍待核對;`coverageNote` 會在季度頁及 API 公開說明本季範圍。 +2019 春季目前整理 29 套 TV 作品(含電視短篇及重編版),夏季整理 38 套 TV 作品及 4 套網絡連載;兩季的其他作品、跨季延續與特殊歌曲仍待核對;`coverageNote` 會在季度頁及 API 公開說明本季範圍。 -目前二十九季快照的 season references、1,945 個作品及 4,298 筆 OP/ED 都有結構化 provenance;4,298 筆歌曲全部至少保留一個 `first_party` 與一個 `cross_check` 來源。`tests/unit/curated-catalog.test.ts` 鎖定來源數量、角色、語言、HTTPS、核對日期、Mock URL 禁止條件,以及 legacy labels 與結構化 ledger 的一致性。 +目前二十九季快照的 season references、1,946 個作品及 4,305 筆 OP/ED 都有結構化 provenance;4,305 筆歌曲全部至少保留一個 `first_party` 與一個 `cross_check` 來源。`tests/unit/curated-catalog.test.ts` 鎖定來源數量、角色、語言、HTTPS、核對日期、Mock URL 禁止條件,以及 legacy labels 與結構化 ledger 的一致性。 ## 欄位決策 diff --git a/docs/PROJECT_PLAN.md b/docs/PROJECT_PLAN.md index 9d188ea..21f9546 100644 --- a/docs/PROJECT_PLAN.md +++ b/docs/PROJECT_PLAN.md @@ -4,8 +4,8 @@ ## Current product scope -- 2026 夏季 70 套、2026 春季 70 套、2026 冬季 66 套、2025 夏季 75 套、2025 春季 82 套、2025 冬季 59 套、2024 秋季 88 套、2024 夏季 68 套、2024 春季 76 套、2024 冬季 75 套、2023 秋季 100 套、夏季 75 套、春季 79 套、冬季 72 套、2022 秋季 80 套、夏季 69 套、春季 79 套、冬季 58 套、2021 秋季 65 套、夏季 46 套、春季 73 套、冬季 67 套、2020 秋季 67 套、夏季 31 套、春季 61 套、冬季 58 套、2019 秋季 67 套、2019 夏季 42 套及 2019 春季 28 套非成人 TV/網絡連載作品; -- 跨季作品共用詳情頁,共 1,945 個唯一作品及 4,298 筆已審閱 OP/ED; +- 2026 夏季 70 套、2026 春季 70 套、2026 冬季 66 套、2025 夏季 75 套、2025 春季 82 套、2025 冬季 59 套、2024 秋季 88 套、2024 夏季 68 套、2024 春季 76 套、2024 冬季 75 套、2023 秋季 100 套、夏季 75 套、春季 79 套、冬季 72 套、2022 秋季 80 套、夏季 69 套、春季 79 套、冬季 58 套、2021 秋季 65 套、夏季 46 套、春季 73 套、冬季 67 套、2020 秋季 67 套、夏季 31 套、春季 61 套、冬季 58 套、2019 秋季 67 套、2019 夏季 42 套及 2019 春季 29 套非成人 TV/網絡連載作品; +- 跨季作品共用詳情頁,共 1,946 個唯一作品及 4,305 筆已審閱 OP/ED; - 五個固定導覽入口、年代/年份/季度目錄、星期一至星期日及不定期分組、分頁本機搜尋、動畫詳情與同源 static JSON API; - 每個季度、作品及歌曲公開來源、語言、review state 及核對日期;每首歌曲同時具備第一方/交叉核對來源; - 官方/licensed YouTube 導航、明確第三方連線同意、來源歸屬及無圖 fallback; @@ -72,6 +72,7 @@ - **v1.40.2 source**:較長的歌曲來源清單改為原生展開區,持續顯示來源數量、審閱狀態與日期;較少來源仍直接顯示,支援獨立展開、鍵盤與無 JavaScript 操作,歌曲、來源及 API 資料不變;發布證據見對應 GitHub Release。 - **v1.41.0 source**:2019 春季增加《鑽石王牌 act2》2019 年 TV 版、6 筆 OP/ED 與 6 筆官方影片 metadata;保留原始演唱者、CD 與 TV Size 日期、共同編曲及官方音源版本,春季共 28 套作品與 69 筆歌曲;發布證據見對應 GitHub Release。 - **v1.41.1 source**:作品頁、季度卡片、搜尋結果、連結標籤及結構化資料省略完全相同的名稱;保留所有不同寫法與原始搜尋/API 資料,支援無 JavaScript 閱讀;發布證據見對應 GitHub Release。 +- **v1.42.0 source**:2019 春季增加《THE ORIGIN 前夜 赤い彗星》13 話電視重編版、7 筆 OP/ED 與 8 筆官方影片 metadata;保留翻唱名義、共同作詞與新編曲、數位/CD 發行日期及第 12 話片尾畫面版本,春季共 29 套作品與 76 筆歌曲;發布證據見對應 GitHub Release。 - 正式網站使用 custom domain;非正式 Cloudflare hostname 不在公開文檔記錄; - 私有 backend、資料庫、crawler、帳戶設定、憑證與內部規則不屬於本公開 repository。 diff --git a/docs/QA_V1_42_2019_SPRING_ORIGIN.md b/docs/QA_V1_42_2019_SPRING_ORIGIN.md new file mode 100644 index 0000000..abbf1da --- /dev/null +++ b/docs/QA_V1_42_2019_SPRING_ORIGIN.md @@ -0,0 +1,32 @@ +# v1.42.0 source — 2019 春季 THE ORIGIN 電視版 + +核對日期:2026-09-07。本文件記錄 source 範圍與本機驗收;正式部署須另以固定版本核對。 + +## 範圍與來源 + +- 新增《機動戦士ガンダム THE ORIGIN 前夜 赤い彗星》2019 年 13 話電視重編版、3 首 OP、4 首 ED 及 8 筆官方影片 metadata。官方首集頁記錄 2019-04-29,NHK 播出時段為每週一 00:35;不使用早期 OVA 或其電影上映日。 +- OP3 保留 LUNA SEA 翻唱、ED2 保留コムアイ、最終 ED 保留アイナ・ジ・エンド演唱;原曲作者與此次編曲者分開。ED1 保留兩位作詞者,前兩首 OP 未核對的製作署名維持空值。 +- 發行日對應原始 CD 或完整版配信,ED 另註 TV Size。第 12 話的 ED3 畫面版本是同一首歌的第二筆影片;不增加一首重複 ED,也不把插曲、OVA 歌曲或 40 週年紀念曲收為本版 OP/ED。 +- 繁中顯示名依 YourAnimes 公開條目交叉核對,保留其原有混合日文寫法,不宣稱為地區官方譯名。歌曲次序由 UZUREA 交叉核對;其 OP3 藝人混列問題由動畫官方與 SUNRISE Music 的 LUNA SEA 署名釐清。 +- 全目錄共 29 季、1,946 個唯一作品、4,305 筆 OP/ED 與 1,802 筆官方/正式授權影片 metadata;春季共 29 套作品、76 筆歌曲。春、夏季仍在補充,2025 秋季尚未收錄。 + +主要證據: + +- [動畫官方 TV 版介紹及 OP/ED](https://www.gundam-the-origin.net/tv/) · [官方首集](https://www.gundam-the-origin.net/tv/episodes01.html) · [YourAnimes](https://youranimes.tw/animes/4690) · [UZUREA](https://uzurea.net/vc/187936/) +- [前兩首 OP 的 CD](https://www.sunrise-music.co.jp/list/detail.php?id=447) · [OP3](https://www.sunrise-music.co.jp/list/detail.php?id=448) · [ED1](https://www.sunrise-music.co.jp/list/detail.php?id=4) · [ED2](https://www.sunrise-music.co.jp/list/detail.php?id=8) · [ED3](https://www.sunrise-music.co.jp/list/detail.php?id=445) · [ED4](https://www.sunrise-music.co.jp/list/detail.php?id=446) +- [第 12 話片尾版本公告](https://www.gundam-the-origin.net/news/index.php?offset=45) + +只保存必要事實、來源連結與官方影片 metadata;沒有新增圖片來源,也沒有下載或重新託管媒體、歌詞及頁面原文。所有新增歌曲保留第一方、交叉核對來源、語言與核對日期。 + +## 驗收 + +- `npm run catalog:check`:207 項通過;`npm run check`:公開邊界、Lint、305 項單元測試、建置、Cloudflare dry-run 及 35 項 Playwright 測試通過,最終瀏覽器測試沒有重試。 +- 完整依賴與正式依賴稽核均為 0 項漏洞;既有 fast-uri 修補與回歸測試繼續通過。dry-run 沒有 runtime bindings。 +- 產生 1,990 份 HTML,公開邊界檢查涵蓋 3,979 份產物。與前一 source 版本相比,1,974 份 API 逐位元相同;只更新春季清單並新增本作 API,沒有移除 API。 +- 新增測試涵蓋 TV 重編版身份、七首歌曲及原始發行日、翻唱演唱者、共同作詞、創作者搜尋,以及 ED3 的兩個畫面版本。瀏覽器測試以鍵盤載入第 12 話的受控影片 fixture,確認另一版本仍可獨立操作,操作前沒有 YouTube iframe 或連線。 +- 1280、960、390 及 320px 共 20 個作品、歌曲及季度畫面檢查通過,沒有水平溢出、頁面錯誤或提前建立的 iframe。人工檢視長標題、翻唱署名、製作名單與影片標題的排列。 +- 既有導覽、年份/季度篩選、分頁搜尋、無 JavaScript 瀏覽、離線閱讀、404 與錯誤狀態回歸通過。初次執行發現的舊總數斷言已更新,再以完整流程驗證。 + +## 發布界線 + +本機結果及分支建置不代表正式網站已更新。正式版本須另外核對合併 SHA、CI、Cloudflare 實際版本與流量,以及 live routes、headers、API 和產物內容;Lighthouse、實機裝置與實際 YouTube 播放不在本機測試的完成宣稱內。 diff --git a/package-lock.json b/package-lock.json index 3c8dc74..5bd91a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "anisonary", - "version": "1.41.1", + "version": "1.42.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "anisonary", - "version": "1.41.1", + "version": "1.42.0", "license": "SEE LICENSE IN LICENSING.md", "dependencies": { "@astrojs/sitemap": "^3.7.3", diff --git a/package.json b/package.json index bfcbf06..3364762 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "anisonary", - "version": "1.41.1", + "version": "1.42.0", "private": true, "license": "SEE LICENSE IN LICENSING.md", "type": "module", diff --git a/src/data/curated-season-registry.ts b/src/data/curated-season-registry.ts index 3a509e5..84e2b3e 100644 --- a/src/data/curated-season-registry.ts +++ b/src/data/curated-season-registry.ts @@ -448,7 +448,7 @@ export const curatedSeasonRegistry = validateCuratedSeasonRegistry([ quarter: "spring", titleZhHant: "春季動畫", titleJa: "2019年春アニメ", - coverageNote: "本季正在補充,目前收錄 28 套 TV 作品(含電視短篇)。其餘作品、跨季延續及特殊歌曲版本仍待核對。", + coverageNote: "本季正在補充,目前收錄 29 套 TV 作品(含電視短篇及重編版)。其餘作品、跨季延續及特殊歌曲版本仍待核對。", seeds: curated2019SpringSeeds, animeIds: curated2019SpringAnimeIds } diff --git a/src/data/curated-seeds/2019/spring.ts b/src/data/curated-seeds/2019/spring.ts index 9f525ae..36d85d1 100644 --- a/src/data/curated-seeds/2019/spring.ts +++ b/src/data/curated-seeds/2019/spring.ts @@ -4,7 +4,7 @@ type Spring2019SeedInput = Omit< CuratedAnimeSeed, | "id" | "seasonIds" | "editorialWeekday" | "status" | "anilistUrl" | "sourceReferenceUrls" | "verifiedAt" ->; +> & Partial>; function spring2019Seed(row: Spring2019SeedInput): CuratedAnimeSeed { const weekday = new Date(`${row.startDate}T00:00:00Z`).getUTCDay(); @@ -16,12 +16,178 @@ function spring2019Seed(row: Spring2019SeedInput): CuratedAnimeSeed { status: "finished", anilistUrl: `https://anilist.co/anime/${row.anilistId}`, wikipediaUrl: row.wikipediaUrl ?? "https://youranimes.tw/bangumi/201904", - sourceReferenceUrls: ["https://acgsecrets.hk/bangumi/201904/"], + sourceReferenceUrls: row.sourceReferenceUrls ?? ["https://acgsecrets.hk/bangumi/201904/"], verifiedAt: "2026-09-07" }; } const curated2019SpringSeedRows: Spring2019SeedInput[] = [ + { + "anilistId": 108039, + "slug": "gundam-the-origin-zenya-akai-suisei", + "startDate": "2019-04-29", + "titleJa": "機動戦士ガンダム THE ORIGIN 前夜 赤い彗星", + "titleZhHant": "機動戰士鋼彈 THE ORIGIN 前夜 赤い彗星", + "titleRomaji": "Kidou Senshi Gundam: THE ORIGIN - Zenya Akai Suisei", + "officialSiteUrl": "https://www.gundam-the-origin.net/tv/", + "wikipediaUrl": "https://youranimes.tw/animes/4690", + "identifierSource": { + "label": "動畫官方首集:2019 年 13 話電視重編版識別與首播日期", + "url": "https://www.gundam-the-origin.net/tv/episodes01.html", + "language": "ja" + }, + "sourceReferenceUrls": [ + "https://uzurea.net/vc/187936/" + ], + "broadcastTimeJst": "00:35", + "broadcastLabel": "NHK 綜合每週一 00:35(日本時間;2019 年 13 話電視重編版)", + "themes": [ + { + "type": "OP", + "sequence": 1, + "titleJa": "宇宙の詩 ~Higher and Higher~", + "artistDisplayName": "LUNA SEA", + "releaseDate": "2019-05-29", + "versionLabel": "2019 年 TV 版 OP,CD 單曲版", + "credits": [] + }, + { + "type": "OP", + "sequence": 2, + "titleJa": "悲壮美", + "artistDisplayName": "LUNA SEA", + "releaseDate": "2019-05-29", + "versionLabel": "2019 年 TV 版 OP,CD 單曲版", + "credits": [] + }, + { + "type": "OP", + "sequence": 3, + "titleJa": "BEYOND THE TIME ~メビウスの宇宙を越えて~", + "artistDisplayName": "LUNA SEA", + "releaseDate": "2019-09-06", + "versionLabel": "LUNA SEA 翻唱版,數位配信", + "credits": [ + { + "name": "小室みつ子", + "role": "lyrics" + }, + { + "name": "小室哲哉", + "role": "composition" + }, + { + "name": "LUNA SEA", + "role": "arrangement" + } + ] + }, + { + "type": "ED", + "sequence": 1, + "titleJa": "めぐりあい", + "artistDisplayName": "SUGIZO feat. GLIM SPANKY", + "releaseDate": "2019-06-11", + "versionLabel": "2019 年翻唱版;完整版配信,串流另提供 TV Size", + "credits": [ + { + "name": "井荻麟", + "role": "lyrics" + }, + { + "name": "売野雅勇", + "role": "lyrics" + }, + { + "name": "井上大輔", + "role": "composition" + }, + { + "name": "SUGIZO", + "role": "arrangement" + } + ] + }, + { + "type": "ED", + "sequence": 2, + "titleJa": "水の星へ愛をこめて", + "artistDisplayName": "SUGIZO feat. コムアイ(水曜日のカンパネラ)", + "releaseDate": "2019-06-18", + "versionLabel": "2019 年翻唱版;完整版配信,串流另提供 TV Size", + "credits": [ + { + "name": "コムアイ(水曜日のカンパネラ)", + "role": "vocals" + }, + { + "name": "売野雅勇", + "role": "lyrics" + }, + { + "name": "ニール・セダカ", + "role": "composition" + }, + { + "name": "SUGIZO", + "role": "arrangement" + } + ] + }, + { + "type": "ED", + "sequence": 3, + "titleJa": "A Red Ray", + "artistDisplayName": "SUGIZO feat. miwa", + "releaseDate": "2019-06-25", + "versionLabel": "完整版配信,串流另提供 TV Size;另有第 12 話片尾畫面版本", + "credits": [ + { + "name": "miwa", + "role": "vocals" + }, + { + "name": "miwa", + "role": "lyrics" + }, + { + "name": "SUGIZO", + "role": "composition" + }, + { + "name": "SUGIZO", + "role": "arrangement" + } + ] + }, + { + "type": "ED", + "sequence": 4, + "titleJa": "光の涯", + "artistDisplayName": "SUGIZO feat. アイナ・ジ・エンド(BiSH)", + "releaseDate": "2019-08-13", + "versionLabel": "最終話 ED,アイナ・ジ・エンド演唱版;完整版配信,串流另提供 TV Size", + "credits": [ + { + "name": "アイナ・ジ・エンド(BiSH)", + "role": "vocals" + }, + { + "name": "MORRIE", + "role": "lyrics" + }, + { + "name": "SUGIZO", + "role": "composition" + }, + { + "name": "SUGIZO", + "role": "arrangement" + } + ] + } + ] + }, { "anilistId": 105749, "slug": "diamond-no-ace-act-ii", diff --git a/src/data/curated-theme-sources/2019/spring.ts b/src/data/curated-theme-sources/2019/spring.ts index 5b32fea..5af9a0c 100644 --- a/src/data/curated-theme-sources/2019/spring.ts +++ b/src/data/curated-theme-sources/2019/spring.ts @@ -2,6 +2,13 @@ import { curated2019SpringSeeds } from "@/data/curated-seeds/2019/spring"; import type { CuratedThemeSourceOverrideMap, CuratedThemeSourceSeed } from "@/data/curated-theme-sources/types"; const firstPartyUrlsByTheme: Readonly> = { + "108039:OP:1": ["https://www.sunrise-music.co.jp/list/detail.php?id=447", "https://www.universal-music.co.jp/luna-sea/news/2019-04-15/"], + "108039:OP:2": ["https://www.sunrise-music.co.jp/list/detail.php?id=447", "https://www.universal-music.co.jp/luna-sea/news/2019-04-15/"], + "108039:OP:3": ["https://www.sunrise-music.co.jp/list/detail.php?id=448"], + "108039:ED:1": ["https://www.sunrise-music.co.jp/list/detail.php?id=4"], + "108039:ED:2": ["https://www.sunrise-music.co.jp/list/detail.php?id=8"], + "108039:ED:3": ["https://www.sunrise-music.co.jp/list/detail.php?id=445", "https://www.gundam-the-origin.net/news/index.php?offset=45", "https://www.sonymusic.co.jp/artist/miwa/info/507943"], + "108039:ED:4": ["https://www.sunrise-music.co.jp/list/detail.php?id=446"], "105749:OP:1": [ "https://www.tv-tokyo.co.jp/broad_tvtokyo/program/detail/201904/22856_201904021755.html", "https://www.glay.co.jp/news/detail/4532", @@ -221,6 +228,15 @@ const firstPartyUrlsByTheme: Readonly> = { }; const sourceLabelsByUrl: Readonly> = { + "https://www.sunrise-music.co.jp/list/detail.php?id=447": "SUNRISE Music:電視版前兩首 OP、CD 單曲日期與官方短版影片", + "https://www.universal-music.co.jp/luna-sea/news/2019-04-15/": "Universal Music:前兩首 OP 與原始 CD 發行日期", + "https://www.sunrise-music.co.jp/list/detail.php?id=448": "SUNRISE Music:LUNA SEA 翻唱 OP、詞曲編曲與數位配信日期", + "https://www.sunrise-music.co.jp/list/detail.php?id=4": "SUNRISE Music:GLIM SPANKY 版 ED、共同作詞、編曲與配信日期", + "https://www.sunrise-music.co.jp/list/detail.php?id=8": "SUNRISE Music:コムアイ 版 ED、詞曲編曲與配信日期", + "https://www.sunrise-music.co.jp/list/detail.php?id=445": "SUNRISE Music:第三首 ED、製作署名與完整/TV Size 配信版本", + "https://www.sunrise-music.co.jp/list/detail.php?id=446": "SUNRISE Music:最終話 ED、個別演唱者、詞曲編曲與配信日期", + "https://www.gundam-the-origin.net/news/index.php?offset=45": "動畫官方:第 12 話片尾畫面版本與最終 ED 歌唱署名", + "https://www.sonymusic.co.jp/artist/miwa/info/507943": "miwa/Sony Music:第三首 ED 與原始配信日期", "https://www.tv-tokyo.co.jp/broad_tvtokyo/program/detail/201904/22856_201904021755.html": "東京電視台:2019 年首播與最初 OP/ED 用途", "https://www.glay.co.jp/news/detail/4532": "GLAY 官方:首支 OP 的詞曲及 TV Size 配信日期", "https://www.glay.co.jp/news/list/4/75/?page=2": "GLAY 官方發行紀錄:2019 年 7 月完整版 CD 收錄", @@ -323,6 +339,12 @@ const sourceLabelsByUrl: Readonly> = { }; const crossCheckSourcesByAnime: Readonly> = { + 108039: { + label: "UZUREA:電視版 OP/ED 次序交叉核對;演唱版本以第一方為準", + url: "https://uzurea.net/vc/187936/", + language: "ja", + role: "cross_check" + }, 105749: { label: "Animate Times:2019 年第三季兩首 OP 與四首 ED 次序交叉核對", url: "https://www.animatetimes.com/news/details.php?id=1776312367&p=3", diff --git a/src/data/curated-theme-videos/2019/spring.ts b/src/data/curated-theme-videos/2019/spring.ts index d9c1547..8fa8328 100644 --- a/src/data/curated-theme-videos/2019/spring.ts +++ b/src/data/curated-theme-videos/2019/spring.ts @@ -1,6 +1,84 @@ import type { CuratedThemeVideoOverrideMap } from "@/data/curated-theme-videos/types"; export const curated2019SpringThemeVideos = { + "108039:OP:1": [ + { + "youtubeVideoId": "z0JjGxZpcWg", + "title": "【公式】『機動戦士ガンダム THE ORIGIN 前夜 赤い彗星』第1弾オープニング", + "type": "other", + "channelName": "サンライズ SUNRISE Official", + "officialStatus": "official", + "embeddable": true + } + ], + "108039:OP:2": [ + { + "youtubeVideoId": "GRDvuQ7KaFI", + "title": "【公式】『機動戦士ガンダム THE ORIGIN 前夜 赤い彗星』第2弾オープニング", + "type": "other", + "channelName": "サンライズ SUNRISE Official", + "officialStatus": "official", + "embeddable": true + } + ], + "108039:OP:3": [ + { + "youtubeVideoId": "MCcZe1R095k", + "title": "【公式】『機動戦士ガンダム THE ORIGIN 前夜 赤い彗星』第3弾オープニング", + "type": "other", + "channelName": "サンライズ SUNRISE Official", + "officialStatus": "official", + "embeddable": true + } + ], + "108039:ED:1": [ + { + "youtubeVideoId": "58wgscVduc0", + "title": "【公式】『機動戦士ガンダム THE ORIGIN 前夜 赤い彗星』第1弾エンディング", + "type": "other", + "channelName": "サンライズ SUNRISE Official", + "officialStatus": "official", + "embeddable": true + } + ], + "108039:ED:2": [ + { + "youtubeVideoId": "uQPP8yKKZ-Y", + "title": "【公式】『機動戦士ガンダム THE ORIGIN 前夜 赤い彗星』第2弾エンディング", + "type": "other", + "channelName": "サンライズ SUNRISE Official", + "officialStatus": "official", + "embeddable": true + } + ], + "108039:ED:3": [ + { + "youtubeVideoId": "h5t3-RSyIzE", + "title": "【公式】『機動戦士ガンダム THE ORIGIN 前夜 赤い彗星』第3弾エンディング", + "type": "other", + "channelName": "サンライズ SUNRISE Official", + "officialStatus": "official", + "embeddable": true + }, + { + "youtubeVideoId": "Nnzm6h4fIRQ", + "title": "【公式】『機動戦士ガンダム THE ORIGIN 前夜 赤い彗星』第3弾エンディング(第12話版)", + "type": "other", + "channelName": "サンライズ SUNRISE Official", + "officialStatus": "official", + "embeddable": true + } + ], + "108039:ED:4": [ + { + "youtubeVideoId": "H9WlcvRRLQY", + "title": "【公式】『機動戦士ガンダム THE ORIGIN 前夜 赤い彗星』第4弾エンディング", + "type": "other", + "channelName": "サンライズ SUNRISE Official", + "officialStatus": "official", + "embeddable": true + } + ], "105749:OP:1": [ { "youtubeVideoId": "J_OxuDSZ4ng", diff --git a/tests/e2e/phase1.spec.ts b/tests/e2e/phase1.spec.ts index 7307689..3c61b2d 100644 --- a/tests/e2e/phase1.spec.ts +++ b/tests/e2e/phase1.spec.ts @@ -460,7 +460,7 @@ test("cross-season search stays local and matches anime, songs, and artists", as await page.goto("/search/"); await expect(page.getByRole("heading", { name: "跨季度搜尋" })).toBeVisible(); - await expect(page.locator("[data-catalog-anime-count]")).toHaveText("1945"); + await expect(page.locator("[data-catalog-anime-count]")).toHaveText("1946"); const search = page.getByRole("searchbox", { name: "搜尋動畫、歌曲或創作者" }); await search.fill("MYTH & ROID"); @@ -500,7 +500,7 @@ test("cross-season search stays local and matches anime, songs, and artists", as await expect(page.locator("[data-catalog-anime-count]")).toHaveText("0"); await search.press("Escape"); - await expect(page.locator("[data-catalog-anime-count]")).toHaveText("1945"); + await expect(page.locator("[data-catalog-anime-count]")).toHaveText("1946"); expect(externalRequests).toEqual([]); }); @@ -888,7 +888,7 @@ test("catalogue navigation stays compact and drills down through published years await expect(navigation.getByRole("link", { name: /動畫目錄/ })).toHaveAttribute("aria-current", "true"); await expect(page.getByRole("navigation", { name: "2025 年季度導覽" }).getByRole("link", { name: "春季" })).toHaveAttribute("aria-current", "page"); await expect(page.getByRole("navigation", { name: "所在位置" }).getByRole("link", { name: "2025", exact: true })).toHaveAttribute("href", "/catalog/2025/"); - await expect(page.getByLabel("季度資料狀態")).toContainText("已發布 29 個季度、1,945 個作品頁與 4,298 首 OP/ED"); + await expect(page.getByLabel("季度資料狀態")).toContainText("已發布 29 個季度、1,946 個作品頁與 4,305 首 OP/ED"); await page.setViewportSize({ width: 390, height: 844 }); await page.reload(); @@ -1124,10 +1124,10 @@ test("2019 spring browsing and creator search reach the correct special ending o for (const width of [1280, 390]) { await page.setViewportSize({ width, height: 844 }); await page.goto("/catalog/2019/"); - await page.getByRole("link", { name: /春季動畫.*28 套動畫/ }).click(); + await page.getByRole("link", { name: /春季動畫.*29 套動畫/ }).click(); await expect(page.getByRole("heading", { name: "2019 春季動畫" })).toBeVisible(); - await expect(page.getByText("本季正在補充,目前收錄 28 套 TV 作品(含電視短篇)。其餘作品、跨季延續及特殊歌曲版本仍待核對。", { exact: true })).toBeVisible(); - await expect(page.locator("[data-anime-card]")).toHaveCount(28); + await expect(page.getByText("本季正在補充,目前收錄 29 套 TV 作品(含電視短篇及重編版)。其餘作品、跨季延續及特殊歌曲版本仍待核對。", { exact: true })).toBeVisible(); + await expect(page.locator("[data-anime-card]")).toHaveCount(29); await expect(page.locator("[data-anime-card] img")).toHaveCount(0); expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); @@ -1508,6 +1508,47 @@ test("YU-NO creator search preserves the exchanged theme singers, release editio expect(mediaRequests).toEqual([]); }); +test("ORIGIN creator search reaches the reviewed cover and keeps the episode-twelve video attached to its ending", async ({ page }) => { + const slug = "gundam-the-origin-zenya-akai-suisei"; + const mediaRequests: string[] = []; + page.on("request", (request) => { + if (/youtube|ytimg|googlevideo/.test(new URL(request.url()).hostname)) mediaRequests.push(request.url()); + }); + for (const width of [1280, 390]) { + await page.setViewportSize({ width, height: 844 }); + await page.goto("/search/"); + for (const [label, value] of [["年份", "2019"], ["季度", "spring"], ["搜尋範圍", "creators"], ["歌曲用途", "ED"]]) { + await page.getByLabel(label!, { exact: true }).selectOption(value!); + } + await page.getByRole("searchbox", { name: "搜尋動畫、歌曲或創作者" }).fill("ニール・セダカ"); + await page.getByRole("link", { name: "水の星へ愛をこめて", exact: true }).click(); + await expect(page).toHaveURL(new RegExp(`/anime/${slug}/#theme-${slug}-ed-2$`)); + const cover = page.locator(`#theme-${slug}-ed-2`); + await expect(cover.locator(".theme-card__artist")).toHaveText("SUGIZO feat. コムアイ(水曜日のカンパネラ)"); + await expect(cover.locator(".theme-card__credits dd")).toHaveText(["コムアイ(水曜日のカンパネラ)", "売野雅勇", "ニール・セダカ", "SUGIZO"]); + await expect(page.locator(`#theme-${slug}-op-3 .theme-card__artist`)).toHaveText("LUNA SEA"); + await expect(page.locator(`#theme-${slug}-op-3 time`)).toHaveText("2019-09-06"); + await expect(page.locator(`#theme-${slug}-ed-1 .theme-card__credits dd`).first()).toHaveText("井荻麟、売野雅勇"); + const third = page.locator(`#theme-${slug}-ed-3`); + await expect(third).toContainText("第12話版"); + await expect(third.getByRole("button", { name: /載入 YouTube 影片/ })).toHaveCount(2); + const finale = page.locator(`#theme-${slug}-ed-4`); + await expect(finale.locator(".theme-card__credits dd")).toHaveText(["アイナ・ジ・エンド(BiSH)", "MORRIE", "SUGIZO", "SUGIZO"]); + await expect(finale).toContainText("最終話 ED"); + await expect(page.locator(".theme-card")).toHaveCount(7); + await expect(page.getByRole("button", { name: /載入 YouTube 影片/ })).toHaveCount(8); + await expect(page.locator("iframe")).toHaveCount(0); + expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); + } + expect(mediaRequests).toEqual([]); + await page.route("https://www.youtube-nocookie.com/**", (route) => route.fulfill({ contentType: "text/html", body: "Episode-twelve ending fixture" })); + await page.locator(`#theme-${slug}-ed-3`).getByRole("button", { name: /載入 YouTube 影片/ }).nth(1).focus(); + await page.keyboard.press("Enter"); + await expect(page.locator("iframe")).toHaveCount(1); + await expect(page.locator("iframe")).toHaveAttribute("src", /youtube-nocookie\.com\/embed\/Nnzm6h4fIRQ/); + await expect(page.locator(`#theme-${slug}-ed-3`).getByRole("button", { name: /載入 YouTube 影片/ })).toHaveCount(1); +}); + test("Diamond creator search reaches the original endings and preserves CD dates, co-arrangers and media versions", async ({ page }) => { const slug = "diamond-no-ace-act-ii"; const mediaRequests: string[] = []; @@ -1755,19 +1796,19 @@ test("weekday navigation follows populated groups and remains useful after filte await page.getByRole("checkbox", { name: "有正版影片" }).check(); await expect(navigation.getByRole("link")).toHaveText(["週一", "週二", "週三", "週四", "週五", "週六", "週日"]); - await expect(page.locator("[data-result-count]")).toHaveText("18"); + await expect(page.locator("[data-result-count]")).toHaveText("19"); for (const link of await navigation.getByRole("link").all()) { const target = page.locator((await link.getAttribute("href"))!); await expect(target).toBeVisible(); const day = await link.getAttribute("href"); - await expect(target.locator("[data-anime-card]:visible")).toHaveCount(day === "#weekday-7" ? 5 : day === "#weekday-5" || day === "#weekday-6" || day === "#weekday-2" ? 3 : day === "#weekday-1" ? 2 : 1); + await expect(target.locator("[data-anime-card]:visible")).toHaveCount(day === "#weekday-7" ? 5 : day === "#weekday-5" || day === "#weekday-6" || day === "#weekday-2" ? 3 : day === "#weekday-1" ? 3 : 1); } await navigation.getByRole("link", { name: "週六" }).click(); await expect(page).toHaveURL(/#weekday-6$/); await expect(navigation.getByRole("link", { name: "週六" })).toHaveAttribute("aria-current", "location"); await page.getByRole("button", { name: "清除篩選" }).click(); await expect(navigation.getByRole("link")).toHaveText(["週一", "週二", "週三", "週四", "週五", "週六", "週日"]); - await expect(page.locator("[data-result-count]")).toHaveText("28"); + await expect(page.locator("[data-result-count]")).toHaveText("29"); await expect(page.getByRole("checkbox", { name: "有 OP" })).toBeFocused(); expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); } @@ -1790,7 +1831,7 @@ test("weekday navigation follows populated groups and remains useful after filte await expect(nativePage.getByRole("navigation", { name: "跳到播出星期" }).getByRole("link")) .toHaveText(["週一", "週二", "週三", "週四", "週五", "週六", "週日"]); await expect(nativePage.locator("[data-weekday-section]")).toHaveCount(7); - await expect(nativePage.locator("[data-anime-card]")).toHaveCount(28); + await expect(nativePage.locator("[data-anime-card]")).toHaveCount(29); await nativePage.getByRole("link", { name: "週五", exact: true }).click(); await expect(nativePage).toHaveURL(/#weekday-5$/); expect(await nativePage.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); diff --git a/tests/unit/api-smoke.test.ts b/tests/unit/api-smoke.test.ts index 22cca7f..2c2bdda 100644 --- a/tests/unit/api-smoke.test.ts +++ b/tests/unit/api-smoke.test.ts @@ -16,7 +16,7 @@ describe("production API smoke gate", () => { it("accepts the complete reviewed catalogue and its not-found behavior", async () => { await expect(smokePublicApi(new CuratedProvider())).resolves.toEqual({ seasonCount: 29, - animeCount: 1945 + animeCount: 1946 }); }); diff --git a/tests/unit/catalog-navigation.test.ts b/tests/unit/catalog-navigation.test.ts index 2af69e9..10cb434 100644 --- a/tests/unit/catalog-navigation.test.ts +++ b/tests/unit/catalog-navigation.test.ts @@ -27,7 +27,7 @@ describe("catalogue navigation and search", () => { expect(results.some(({ anime }) => anime.slug === "re-zero-season-4")).toBe(true); } expect(searchCatalog(index, { ...defaults, year: "2025", quarter: "fall" })).toEqual([]); - expect(searchCatalog(index, defaults)).toHaveLength(1945); + expect(searchCatalog(index, defaults)).toHaveLength(1946); }); it("combines multilingual queries with exact song, creator, and OP/ED filters", () => { diff --git a/tests/unit/curated-2019-spring.test.ts b/tests/unit/curated-2019-spring.test.ts index e319ff4..0157dd5 100644 --- a/tests/unit/curated-2019-spring.test.ts +++ b/tests/unit/curated-2019-spring.test.ts @@ -10,6 +10,74 @@ const spring = curatedSeasonDetails.find(({ id }) => id === "2019-spring")!; const anime = (id: number) => curatedAnimeDetails.find((item) => item.id === `curated-${id}`)!; describe("2019 spring reviewed TV catalogue", () => { + it("identifies THE ORIGIN as the thirteen-part TV recut with its Monday premiere and independent public evidence", async () => { + const seed = curated2019SpringSeeds.find(({ anilistId }) => anilistId === 108039)!; + expect(seed).toMatchObject({ startDate: "2019-04-29", seasonIds: ["2019-spring"] }); + expect(seed.sourceReferenceUrls).toEqual(["https://uzurea.net/vc/187936/"]); + const detail = anime(108039); + expect(detail).toMatchObject({ editorialWeekday: 1, broadcastTimeJst: "00:35" }); + expect(detail.broadcastLabel).toContain("13 話電視重編版"); + expect(detail.sources).toContainEqual(expect.objectContaining({ + url: "https://www.gundam-the-origin.net/tv/episodes01.html", language: "ja", role: "identifier" + })); + expect(detail.posterUrl).toBeUndefined(); + expect(detail.imageSourceUrl).toBeUndefined(); + const provider = new ApiProvider("https://anisonary.k-y.cc/api/v1", { + fetch: vi.fn().mockResolvedValue(new Response(JSON.stringify(detail), { headers: { "Content-Type": "application/json" } })) + }); + expect(await provider.getAnime(detail.slug)).toEqual(detail); + }); + + it("preserves THE ORIGIN's seven TV themes and their original releases instead of other singers or anniversary-album dates", () => { + const themes = anime(108039).themes; + expect(themes.map(({ type, sequence, titleJa, artistDisplayName, releaseDate }) => [type, sequence, titleJa, artistDisplayName, releaseDate])).toEqual([ + ["OP", 1, "宇宙の詩 ~Higher and Higher~", "LUNA SEA", "2019-05-29"], + ["OP", 2, "悲壮美", "LUNA SEA", "2019-05-29"], + ["OP", 3, "BEYOND THE TIME ~メビウスの宇宙を越えて~", "LUNA SEA", "2019-09-06"], + ["ED", 1, "めぐりあい", "SUGIZO feat. GLIM SPANKY", "2019-06-11"], + ["ED", 2, "水の星へ愛をこめて", "SUGIZO feat. コムアイ(水曜日のカンパネラ)", "2019-06-18"], + ["ED", 3, "A Red Ray", "SUGIZO feat. miwa", "2019-06-25"], + ["ED", 4, "光の涯", "SUGIZO feat. アイナ・ジ・エンド(BiSH)", "2019-08-13"] + ]); + expect(themes[0]?.versionLabel).toContain("CD 單曲版"); + expect(themes[2]?.versionLabel).toContain("LUNA SEA 翻唱版"); + for (const index of [3, 4, 5, 6]) expect(themes[index]?.versionLabel).toContain("TV Size"); + expect(themes[6]?.versionLabel).toContain("最終話 ED"); + expect(themes.some(({ titleJa }) => ["THE BEYOND", "By Your Side", "Don't Say Good bye", "破線の涙"].includes(titleJa))).toBe(false); + }); + + it("keeps THE ORIGIN's original songwriters, new arrangements and featured singers separate", () => { + const themes = anime(108039).themes; + expect(themes[0]?.credits).toEqual([]); + expect(themes[1]?.credits).toEqual([]); + expect(themes[2]?.credits).toEqual([ + { name: "小室みつ子", role: "lyrics" }, { name: "小室哲哉", role: "composition" }, { name: "LUNA SEA", role: "arrangement" } + ]); + expect(themes[3]?.credits.filter(({ role }) => role === "lyrics").map(({ name }) => name)).toEqual(["井荻麟", "売野雅勇"]); + expect(themes[4]?.credits).toContainEqual({ name: "ニール・セダカ", role: "composition" }); + expect(themes[4]?.credits).toContainEqual({ name: "コムアイ(水曜日のカンパネラ)", role: "vocals" }); + expect(themes[5]?.credits).toContainEqual({ name: "miwa", role: "lyrics" }); + expect(themes[6]?.credits).toContainEqual({ name: "MORRIE", role: "lyrics" }); + expect(themes[6]?.credits.filter(({ role }) => role === "vocals")).toEqual([{ name: "アイナ・ジ・エンド(BiSH)", role: "vocals" }]); + for (const index of [3, 4, 5, 6]) expect(themes[index]?.credits).toContainEqual({ name: "SUGIZO", role: "arrangement" }); + for (const theme of themes) expect(theme.sources).toContainEqual(expect.objectContaining({ + url: "https://uzurea.net/vc/187936/", language: "ja", role: "cross_check" + })); + }); + + it("finds the new ORIGIN cover by songwriter and keeps the episode-twelve ending as a second video for the same song", async () => { + const detail = anime(108039); + const index = buildCatalogSearchIndex((await loadCatalogSearchData(new CuratedProvider(), true)).entries); + const result = searchCatalog(index, { query: "ニール・セダカ", scope: "creators", year: "2019", quarter: "spring", type: "ED" }) + .find(({ anime: item }) => item.slug === detail.slug); + expect(result?.themes.map(({ titleJa }) => titleJa)).toEqual(["水の星へ愛をこめて"]); + expect(detail.themes[5]?.videos.map(({ youtubeVideoId }) => youtubeVideoId)).toEqual(["h5t3-RSyIzE", "Nnzm6h4fIRQ"]); + expect(detail.themes[5]?.videos[1]?.title).toContain("第12話版"); + const videos = detail.themes.flatMap(({ videos }) => videos); + expect(videos).toHaveLength(8); + expect(videos.every(({ type, officialStatus }) => type === "other" && officialStatus === "official")).toBe(true); + }); + it("keeps the 2019 Diamond no Ace act II identity separate from its later sequel and round-trips its public detail", async () => { const seed = curated2019SpringSeeds.find(({ anilistId }) => anilistId === 105749)!; expect(seed).toMatchObject({ startDate: "2019-04-02", seasonIds: ["2019-spring"] }); @@ -367,9 +435,9 @@ describe("2019 spring reviewed TV catalogue", () => { }); it("publishes a partial quarter with reviewed identity and song evidence, without unverified artwork", () => { - expect(spring.anime).toHaveLength(28); + expect(spring.anime).toHaveLength(29); expect(spring.coverageNote).toContain("跨季延續及特殊歌曲版本仍待核對"); - expect(curated2019SpringSeeds.flatMap(({ themes }) => themes)).toHaveLength(69); + expect(curated2019SpringSeeds.flatMap(({ themes }) => themes)).toHaveLength(76); expect(curatedSeasonDetails.some(({ id }) => id === "2025-fall")).toBe(false); for (const seed of curated2019SpringSeeds) { expect(seed.seasonIds).toEqual(["2019-spring"]); diff --git a/tests/unit/curated-catalog.test.ts b/tests/unit/curated-catalog.test.ts index 66f5395..9cb0c18 100644 --- a/tests/unit/curated-catalog.test.ts +++ b/tests/unit/curated-catalog.test.ts @@ -38,7 +38,7 @@ describe("curated public catalogue", () => { "2019-spring" ]); expect(curatedSeasonDetails).toHaveLength(29); - expect(curatedSeasonDetails.map((season) => season.anime.length)).toEqual([70, 70, 66, 75, 82, 59, 88, 68, 76, 75, 100, 75, 79, 72, 80, 69, 79, 58, 65, 46, 73, 67, 67, 31, 61, 58, 67, 42, 28]); + expect(curatedSeasonDetails.map((season) => season.anime.length)).toEqual([70, 70, 66, 75, 82, 59, 88, 68, 76, 75, 100, 75, 79, 72, 80, 69, 79, 58, 65, 46, 73, 67, 67, 31, 61, 58, 67, 42, 29]); expect(curatedSeasonDetails.map((season) => [season.id, season.reviewState, season.verifiedAt])).toEqual([ ["2026-summer", "reviewed", "2026-08-02"], ["2026-spring", "reviewed", "2026-08-02"], @@ -70,10 +70,10 @@ describe("curated public catalogue", () => { ["2019-summer", "reviewed", "2026-09-07"], ["2019-spring", "reviewed", "2026-09-07"] ]); - expect(curatedAnimeDetails).toHaveLength(1945); - expect(curatedAnimeDetails.filter((anime) => anime.themes.length > 0)).toHaveLength(1581); - expect(curatedAnimeDetails.flatMap((anime) => anime.themes)).toHaveLength(4298); - expect(curatedAnimeDetails.filter((anime) => anime.themeAvailability === "documented")).toHaveLength(1581); + expect(curatedAnimeDetails).toHaveLength(1946); + expect(curatedAnimeDetails.filter((anime) => anime.themes.length > 0)).toHaveLength(1582); + expect(curatedAnimeDetails.flatMap((anime) => anime.themes)).toHaveLength(4305); + expect(curatedAnimeDetails.filter((anime) => anime.themeAvailability === "documented")).toHaveLength(1582); expect(curatedAnimeDetails.filter((anime) => anime.themeAvailability === "not_used")).toHaveLength(2); expect(curatedAnimeDetails.filter((anime) => anime.themeAvailability === "not_announced")).toHaveLength(362); const youtubeLinks = curatedAnimeDetails diff --git a/tests/unit/curated-season-registry.test.ts b/tests/unit/curated-season-registry.test.ts index 54db5b3..7f88570 100644 --- a/tests/unit/curated-season-registry.test.ts +++ b/tests/unit/curated-season-registry.test.ts @@ -39,7 +39,7 @@ describe("curated season registry", () => { ]); const ownedSeeds = curatedSeasonRegistry.flatMap(({ seeds }) => seeds); - expect(ownedSeeds).toHaveLength(1945); + expect(ownedSeeds).toHaveLength(1946); expect(new Set(ownedSeeds.map(({ anilistId }) => anilistId)).size).toBe(ownedSeeds.length); for (const entry of curatedSeasonRegistry) { diff --git a/tests/unit/curated-theme-source-registry.test.ts b/tests/unit/curated-theme-source-registry.test.ts index 835d4ef..428af21 100644 --- a/tests/unit/curated-theme-source-registry.test.ts +++ b/tests/unit/curated-theme-source-registry.test.ts @@ -69,11 +69,11 @@ describe("curated theme source registry", () => { 180, 161, 103, - 69 + 76 ]); const keys = curatedThemeSourceRegistry.flatMap(({ overrides }) => Object.keys(overrides)); - expect(keys).toHaveLength(2036); + expect(keys).toHaveLength(2043); expect(new Set(keys).size).toBe(keys.length); expect(Object.keys(curatedThemeSourceOverrides)).toHaveLength(keys.length); }); diff --git a/tests/unit/curated-theme-video-registry.test.ts b/tests/unit/curated-theme-video-registry.test.ts index 3fd4ebb..fe17cd7 100644 --- a/tests/unit/curated-theme-video-registry.test.ts +++ b/tests/unit/curated-theme-video-registry.test.ts @@ -47,15 +47,15 @@ describe("curated theme video registry", () => { ), ).toEqual([ 4, 10, 44, 178, 79, 48, 128, 154, 134, 125, 2, 4, 6, 7, 27, 28, 26, 26, - 28, 65, 66, 53, 62, 52, 102, 43, 62, 11, 37, + 28, 65, 66, 53, 62, 52, 102, 43, 62, 11, 44, ]); const entries = curatedThemeVideoRegistry.flatMap(({ overrides }) => Object.entries(overrides), ); - expect(entries).toHaveLength(1611); + expect(entries).toHaveLength(1618); expect(new Set(entries.map(([key]) => key)).size).toBe(entries.length); - expect(entries.flatMap(([, videos]) => videos)).toHaveLength(1794); + expect(entries.flatMap(([, videos]) => videos)).toHaveLength(1802); expect(Object.keys(curatedThemeVideoOverrides)).toHaveLength( entries.length, ); diff --git a/tests/unit/page-data.test.ts b/tests/unit/page-data.test.ts index d6abbe7..5545c2a 100644 --- a/tests/unit/page-data.test.ts +++ b/tests/unit/page-data.test.ts @@ -65,9 +65,9 @@ describe("catalogue search data states", () => { expect(result.status).toBe("ready"); if (result.status !== "ready") throw new Error("Expected ready catalogue search data"); - expect(result.entries).toHaveLength(1945); - expect(new Set(result.entries.map(({ anime }) => anime.slug)).size).toBe(1945); - expect(result.entries.filter(({ anime }) => anime.themes.length > 0)).toHaveLength(1581); + expect(result.entries).toHaveLength(1946); + expect(new Set(result.entries.map(({ anime }) => anime.slug)).size).toBe(1946); + expect(result.entries.filter(({ anime }) => anime.themes.length > 0)).toHaveLength(1582); }); it("returns a safe public error state without exposing provider details", async () => {