From df8febd56c3123d6e014464729792166e26f069c Mon Sep 17 00:00:00 2001 From: kyeunga25 <49793173+kyeunga25@users.noreply.github.com> Date: Tue, 8 Sep 2026 01:33:14 +0800 Subject: [PATCH] (feat): add 2019 spring CLIMAX SEASON endings --- CHANGELOG.md | 6 ++ README.md | 6 +- docs/DATA_PROVENANCE.md | 4 +- docs/DATA_SOURCES.md | 4 +- docs/PROJECT_PLAN.md | 5 +- docs/QA_V1_50_2019_SPRING_CLIMAX.md | 28 ++++++ package-lock.json | 4 +- package.json | 2 +- src/data/catalog-sources.ts | 2 +- src/data/curated-season-registry.ts | 2 +- src/data/curated-seeds/2019/spring.ts | 68 +++++++++++++ src/data/curated-theme-sources/2019/spring.ts | 24 +++++ src/data/curated-theme-videos/2019/spring.ts | 24 +++++ tests/e2e/phase1.spec.ts | 99 ++++++++++++++++--- tests/unit/api-smoke.test.ts | 2 +- tests/unit/catalog-navigation.test.ts | 2 +- tests/unit/curated-2019-spring.test.ts | 87 +++++++++++++++- tests/unit/curated-catalog.test.ts | 16 +-- 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 +- 22 files changed, 353 insertions(+), 50 deletions(-) create mode 100644 docs/QA_V1_50_2019_SPRING_CLIMAX.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 1aa113a..8342d8a 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.50.0 — 2019 春季《灰姑娘女孩劇場 CLIMAX SEASON》 + +- 收錄 2019 年 TV 短篇系列與四、五、六月的三首輪替 ED;保留五人合唱名義、個別演唱者、已核對製作署名及 CD 完整版日期。 +- 新增三筆日本 Columbia 官方試聽影片 metadata,標明短版;CD 獨唱混音、附加曲、遊戲限定短篇及《Extra Stage》保持獨立。 +- 春季共 34 套作品、99 筆 OP/ED;本季與夏季仍在補充,2025 秋季尚未收錄。 + ## v1.49.2 — 手機首頁入口 - 手機首頁先顯示介紹、季度/年份瀏覽與搜尋入口,縮短首屏上下間距;裝飾示意圖保留於較大版面。 diff --git a/README.md b/README.md index 647c4bf..80163e8 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,950 unique titles and 4,325 known OP/ED records. The 2019 spring snapshot contains thirty-three 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. +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,951 unique titles and 4,328 known OP/ED records. The 2019 spring snapshot contains thirty-four 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.49.2**。手機首頁優先顯示網站介紹、季度/年份入口與搜尋連結,縮短進入動畫目錄前的捲動距離。目錄共有 29 個季度、1,950 套作品與 4,325 筆 OP/ED;春季為 33 套作品、96 筆歌曲,春、夏季仍在補充,2025 秋季尚未收錄。 +目前 source 版本:**v1.50.0**。新增 2019 春季《灰姑娘女孩劇場 第四季:CLIMAX SEASON》、三首每月輪替 ED 與官方試聽短版,保留五人合唱名義及各人署名。目錄共有 29 個季度、1,951 套作品與 4,328 筆 OP/ED;春季為 34 套作品、99 筆歌曲,春、夏季仍在補充,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.49.2`;正式發布狀態以對應 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.50.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 6ed2e68..e8e5204 100644 --- a/docs/DATA_PROVENANCE.md +++ b/docs/DATA_PROVENANCE.md @@ -31,9 +31,9 @@ API、資料庫、商標或媒體重用權。完整授權邊界見 `first_party` 表示動畫製作/發行方、唱片公司、藝人官方渠道、官方影片或正式播出機構的公開頁面。`cross_check` 表示用於發現缺口及交叉比對的公開索引。兩種角色都是來源分類,不代表數值信心或內容評分。 -2019 春季目前整理 33 套 TV 作品(含電視短篇及重編版),夏季整理 38 套 TV 作品及 4 套網絡連載;兩季的其他作品、跨季延續與特殊歌曲仍待核對;`coverageNote` 會在季度頁及 API 公開說明本季範圍。 +2019 春季目前整理 34 套 TV 作品(含電視短篇及重編版),夏季整理 38 套 TV 作品及 4 套網絡連載;兩季的其他作品、跨季延續與特殊歌曲仍待核對;`coverageNote` 會在季度頁及 API 公開說明本季範圍。 -目前二十九季快照的 season references、1,950 個作品及 4,325 筆 OP/ED 都有結構化 provenance;4,325 筆歌曲全部至少保留一個 `first_party` 與一個 `cross_check` 來源。`tests/unit/curated-catalog.test.ts` 鎖定來源數量、角色、語言、HTTPS、核對日期、Mock URL 禁止條件,以及 legacy labels 與結構化 ledger 的一致性。 +目前二十九季快照的 season references、1,951 個作品及 4,328 筆 OP/ED 都有結構化 provenance;4,328 筆歌曲全部至少保留一個 `first_party` 與一個 `cross_check` 來源。`tests/unit/curated-catalog.test.ts` 鎖定來源數量、角色、語言、HTTPS、核對日期、Mock URL 禁止條件,以及 legacy labels 與結構化 ledger 的一致性。 ## 欄位決策 diff --git a/docs/DATA_SOURCES.md b/docs/DATA_SOURCES.md index 92e57fb..90a02cb 100644 --- a/docs/DATA_SOURCES.md +++ b/docs/DATA_SOURCES.md @@ -11,13 +11,13 @@ repository 的 Apache-2.0 軟件授權範圍內,詳見 季度完整性定義為:在日本於該季度日曆月份首播的非成人 `TV`/`WEB` 連載動畫,包含短篇、新季度與分割 cour;排除劇場版、OVA、宣傳短片及 18+ 作品。正式播出/配信的獨立特別篇只有在至少兩個季度來源一致歸入該季、並有可追溯第一方發布時才收錄。若作品在季度開始前數日先行發布,亦須由至少兩個季度目錄或主題曲索引一致歸入該季。同一作品跨季時共用一個詳情頁。 -目前 repository 中已審閱的快照為:2026 夏季 70 套、2026 春季 70 套、2026 冬季 66 套、2025 夏季 75 套、2025 春季 82 套、2025 冬季 59 套、2024 秋季 88 套、2024 夏季 68 套、2024 春季 76 套、2024 冬季 75 套、2023 秋季 100 套、2023 夏季 75 套、2023 春季 79 套、2023 冬季 72 套、2022 秋季 80 套、2022 夏季 69 套、2022 春季 79 套、2022 冬季 58 套、2021 秋季 65 套、2021 夏季 46 套、2021 春季 73 套、2021 冬季 67 套、2020 秋季 67 套、2020 夏季 31 套、2020 春季 61 套、2020 冬季 58 套、2019 秋季 67 套、2019 夏季 42 套(38 套 TV、4 套網絡連載)及 2019 春季 33 套 TV 作品(含電視短篇及重編版),共 1,950 個作品詳情、4,325 筆 OP/ED 與 567 個歌曲層 YouTube 直連;其中 1,808 筆已保留官方或正式授權影片 metadata。 +目前 repository 中已審閱的快照為:2026 夏季 70 套、2026 春季 70 套、2026 冬季 66 套、2025 夏季 75 套、2025 春季 82 套、2025 冬季 59 套、2024 秋季 88 套、2024 夏季 68 套、2024 春季 76 套、2024 冬季 75 套、2023 秋季 100 套、2023 夏季 75 套、2023 春季 79 套、2023 冬季 72 套、2022 秋季 80 套、2022 夏季 69 套、2022 春季 79 套、2022 冬季 58 套、2021 秋季 65 套、2021 夏季 46 套、2021 春季 73 套、2021 冬季 67 套、2020 秋季 67 套、2020 夏季 31 套、2020 春季 61 套、2020 冬季 58 套、2019 秋季 67 套、2019 夏季 42 套(38 套 TV、4 套網絡連載)及 2019 春季 34 套 TV 作品(含電視短篇及重編版),共 1,951 個作品詳情、4,328 筆 OP/ED 與 567 個歌曲層 YouTube 直連;其中 1,811 筆已保留官方或正式授權影片 metadata。 每季先以繁中年度動畫表的對應月份建立日曆清單,再與 Annict、Bangumi 及 AniList identifier 取聯集核對。OP/ED 以作品官網和官方 YouTube 為主,並用 AnimeThemes、UZUREA 與 UtaTen 等季度主題曲表交叉確認;季度歌曲索引未收錄的作品,須返回 Netflix、唱片公司、IP 官網、正式播出機構或製作公司等第一方頁面補證。台港正式代理、串流或出版社名稱優先;未有可確認的繁中名稱時保留原文,不宣稱為正式授權譯名。 2019 春、夏季仍在補充,2025 秋季尚未收錄;季度快照代表已核對範圍,不代表歷史作品或歌曲已全部補齊。 -每首已發布歌曲在 `PublicTheme.sources[]` 留下 URL、語言、`first_party`/`cross_check` 角色及核對日期。二十九季快照的 4,325 筆歌曲都必須同時具備兩種角色;這是公開 provenance gate,不是 confidence score。第三方索引將歌曲標為 OP/ED、但第一方頁面只證實插曲、劇中歌、Victory Dance、其他節目用途、舊季度歌曲、其他改編版本或一般「主題歌」時,不把它升格為官方 OP/ED;在既有導航契約中保留的一般主題曲會明示「主題歌(官方分類)」。完整欄位與媒體邊界見 [`DATA_PROVENANCE.md`](./DATA_PROVENANCE.md)。 +每首已發布歌曲在 `PublicTheme.sources[]` 留下 URL、語言、`first_party`/`cross_check` 角色及核對日期。二十九季快照的 4,328 筆歌曲都必須同時具備兩種角色;這是公開 provenance gate,不是 confidence score。第三方索引將歌曲標為 OP/ED、但第一方頁面只證實插曲、劇中歌、Victory Dance、其他節目用途、舊季度歌曲、其他改編版本或一般「主題歌」時,不把它升格為官方 OP/ED;在既有導航契約中保留的一般主題曲會明示「主題歌(官方分類)」。完整欄位與媒體邊界見 [`DATA_PROVENANCE.md`](./DATA_PROVENANCE.md)。 ## 固定來源 diff --git a/docs/PROJECT_PLAN.md b/docs/PROJECT_PLAN.md index c28f22f..be30ed0 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 春季 33 套非成人 TV/網絡連載作品; -- 跨季作品共用詳情頁,共 1,950 個唯一作品及 4,325 筆已審閱 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 春季 34 套非成人 TV/網絡連載作品; +- 跨季作品共用詳情頁,共 1,951 個唯一作品及 4,328 筆已審閱 OP/ED; - 五個固定導覽入口、年代/年份/季度目錄、星期一至星期日及不定期分組、分頁本機搜尋、動畫詳情與同源 static JSON API; - 每個季度、作品及歌曲公開來源、語言、review state 及核對日期;每首歌曲同時具備第一方/交叉核對來源; - 官方/licensed YouTube 導航、明確第三方連線同意、來源歸屬及無圖 fallback; @@ -84,6 +84,7 @@ - **v1.49.0 source**:2019 春季增加《妖怪手錶!》、2 筆 OP/ED 及 3 筆官方影片 metadata;區分 2019/2021 年系列、共同作詞、原曲與樂譜改編,以及 CD 日期與短版影片,春季共 33 套作品與 96 筆歌曲;發布證據見對應 GitHub Release。 - **v1.49.1 source**:改善淺色文字、來源與選取狀態的對比,深色 OP/ED 標籤及滑鼠移入按鈕使用適合彩色背景的文字;搜尋提示更清楚,首頁標題可換行以避免平板溢出,全域導覽、歌曲資料與 API 不變,發布證據見對應 GitHub Release。 - **v1.49.2 source**:手機首頁優先呈現介紹、季度/年份及搜尋入口,縮短首屏間距,較大版面保留示意圖;支援原生目錄與無 JavaScript 導覽,資料與 API 不變,發布證據見對應 GitHub Release。 +- **v1.50.0 source**:2019 春季增加《灰姑娘女孩劇場 第四季:CLIMAX SEASON》與 3 首月替 ED、3 筆官方試聽 metadata;保留角色合唱、個別演唱者與 CD 日期,遊戲限定短篇、CD 獨唱混音及附加曲不作 TV 歌曲,春季共 34 套作品與 99 筆歌曲;發布證據見對應 GitHub Release。 - 正式網站使用 custom domain;非正式 Cloudflare hostname 不在公開文檔記錄; - 私有 backend、資料庫、crawler、帳戶設定、憑證與內部規則不屬於本公開 repository。 diff --git a/docs/QA_V1_50_2019_SPRING_CLIMAX.md b/docs/QA_V1_50_2019_SPRING_CLIMAX.md new file mode 100644 index 0000000..483f4fa --- /dev/null +++ b/docs/QA_V1_50_2019_SPRING_CLIMAX.md @@ -0,0 +1,28 @@ +# v1.50.0|2019 春季《灰姑娘女孩劇場 CLIMAX SEASON》QA + +來源與本機核對:2026-09-08。本文件記錄 source 範圍;正式部署須於固定版本合併後獨立驗收。 + +## 資料與來源 + +- 新增《アイドルマスター シンデレラガールズ劇場 CLIMAX SEASON/灰姑娘女孩劇場 第四季:CLIMAX SEASON》2019 年 TV 短篇系列、三首每月輪替 ED 與三筆官方試聽影片 metadata;春季共 34 套作品、99 筆歌曲,仍明示季度未完成。 +- [Bandai Namco 官方公告](https://idolmaster.jp/blog/?p=58033)確認日本首播為 2019-04-02、星期二 21:54。[系列官方資料](https://idolmaster.jp/blog/?p=58824)明確分開 13 話 TV 版及 7 話遊戲限定版;[YourAnimes 季表](https://youranimes.tw/bangumi/201904)與[作品頁](https://youranimes.tw/animes/1922)交叉核對繁中名稱、日期、話數及播映完畢狀態。社群名稱不標成官方地區譯名。 +- 日本 Columbia 的[四月公告](https://columbia.jp/idolmaster/imasnews/190405.html)、[五月公告](https://columbia.jp/idolmaster/imasnews/190510.html)與[六月公告](https://columbia.jp/idolmaster/imasnews/190607.html)逐曲確認〈きゅん・きゅん・まっくす〉、〈Max Beat〉及〈TAKAMARI☆CLIMAXXX!!!!!〉的 TV ED 用途、五人演唱名義、個別配音演唱者與 CD 日期 2019-04-17、2019-05-22、2019-06-19。 +- 四月 ED 保留作詞坂井竜二與作曲 BNSI(kyo);TV/CD 編曲欄位尚缺直接第一方核對,未由其他使用版本補值。五月 ED 保留渡部紫緒作詞、坂部剛作曲與編曲;六月 ED 三項職務均保留広川恵一(MONACA)。[アニソン・オンライン](https://anison.online/anime/1069)交叉核對季度、ED 用途與合唱名義。 +- 唱片公告明確分開五人合唱、個人獨唱混音、伴奏及附加曲;後三者不新增為 TV 歌曲,不猜測單集範圍。遊戲限定短篇與既有 2020 年《Extra Stage》保持獨立。 +- [四月](https://www.youtube.com/watch?v=QkO1DC96kIM)、[五月](https://www.youtube.com/watch?v=bqUcdQSW3qA)、[六月](https://www.youtube.com/watch?v=gBvq0uP4Hos)官方試聽分別為 85、77、90 秒,核對公開標題、頻道及可嵌入狀態,只保存必要 metadata。三段均標示試聽短版,不作完整 MV 或無字幕片尾。 +- 沿用來源矩陣的必要事實使用範圍;不生成外部 ID、Romaji、圖片、演唱版本或未核對署名,不保存頁面原文、完整歌詞、音訊或影片。既有作品與歌曲的來源日期不變。 + +## 本機驗收 + +- `npm run catalog:check`:240 項通過。 +- `npm run check -- -- --workers=1`:公開邊界、Lint、343 項單元測試、建置、Cloudflare dry-run 與 49 項 Playwright 通過;最終完整瀏覽器測試沒有重試。 +- 完整依賴與正式依賴稽核均為 0 項漏洞;`fast-uri` 保持 3.1.6,既有安全回歸通過。 +- 與 v1.49.2 source 比較:1,979 份 API 逐位元相同,只更新春季清單並新增作品詳情,沒有移除。既有《Extra Stage》詳情及歌曲不變。建置產生 1,995 份 HTML、3,989 份公開產物,dry-run 沒有 runtime bindings。 +- 四項新資料測試先確認缺少記錄時失敗,再驗證獨立身份、每月 ED 用途、五人合唱、製作署名、CD 日期、來源、影片、API 讀回及繁中/創作者搜尋。 +- 新 E2E 涵蓋 1280、960、390、320px 的年份/季度篩選、個別演唱者連結、歌曲深連結、鍵盤來源展開與媒體同意,以及 390px 無 JavaScript 的年度、季度、作品及來源導覽。影片同意使用固定回應檢查 iframe 目標;既有離線、404、對比與導覽回歸通過。 +- 初次新 E2E 遺漏展開來源清單的步驟,已依原生操作補上鍵盤展開;專項與最終完整測試通過,未改動頁面行為或降低可見性斷言。 +- Chromium 檢查作品、三首 ED 及春季頁共 25 個畫面,包含手機無 JavaScript 閱讀與來源展開;沒有水平溢出、頁面錯誤或操作前的 iframe/YouTube 請求。人工檢視桌面片尾及手機作品、來源展開截圖。 + +## 發布界線 + +本機與分支建置結果不代表正式網站已更新。發布時仍須核對合併 SHA、CI、Cloudflare 實際版本與流量,以及 live routes、headers、API 與產物內容。本切片未驗證實體裝置、Safari、Firefox、Lighthouse/Core Web Vitals 或實際 YouTube 播放。 diff --git a/package-lock.json b/package-lock.json index 74a6334..d92c6ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "anisonary", - "version": "1.49.2", + "version": "1.50.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "anisonary", - "version": "1.49.2", + "version": "1.50.0", "license": "SEE LICENSE IN LICENSING.md", "dependencies": { "@astrojs/sitemap": "^3.7.3", diff --git a/package.json b/package.json index 9c07c8a..c706467 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "anisonary", - "version": "1.49.2", + "version": "1.50.0", "private": true, "license": "SEE LICENSE IN LICENSING.md", "type": "module", diff --git a/src/data/catalog-sources.ts b/src/data/catalog-sources.ts index 5b020e0..b6a81d0 100644 --- a/src/data/catalog-sources.ts +++ b/src/data/catalog-sources.ts @@ -79,7 +79,7 @@ export const seasonSnapshotVerifiedAt = { "2020-winter": "2026-09-02", "2019-fall": "2026-09-02", "2019-summer": "2026-09-07", - "2019-spring": "2026-09-07" + "2019-spring": "2026-09-08" } as const; function annictQuarterName(quarter: Quarter): Quarter | "autumn" { diff --git a/src/data/curated-season-registry.ts b/src/data/curated-season-registry.ts index 7427093..c32d1ac 100644 --- a/src/data/curated-season-registry.ts +++ b/src/data/curated-season-registry.ts @@ -452,7 +452,7 @@ export const curatedSeasonRegistry = validateCuratedSeasonRegistry([ quarter: "spring", titleZhHant: "春季動畫", titleJa: "2019年春アニメ", - coverageNote: "本季正在補充,目前收錄 33 套 TV 作品(含電視短篇及重編版)。其餘作品、跨季延續及特殊歌曲版本仍待核對。", + coverageNote: "本季正在補充,目前收錄 34 套 TV 作品(含電視短篇及重編版)。其餘作品、跨季延續及特殊歌曲版本仍待核對。", seeds: curated2019SpringSeeds, animeIds: curated2019SpringAnimeIds } diff --git a/src/data/curated-seeds/2019/spring.ts b/src/data/curated-seeds/2019/spring.ts index 360fdfd..52359cf 100644 --- a/src/data/curated-seeds/2019/spring.ts +++ b/src/data/curated-seeds/2019/spring.ts @@ -1698,6 +1698,74 @@ const strikeWitches501EndingRows = [ ] as const; const independentSpringSeeds: NativeCuratedAnimeSeed[] = [{ + id: "catalog-cinderella-girls-climax-2019", + slug: "cinderella-girls-climax-2019", + titleJa: "アイドルマスター シンデレラガールズ劇場 CLIMAX SEASON", + titleZhHant: "灰姑娘女孩劇場 第四季:CLIMAX SEASON", + seasonIds: ["2019-spring"], + startDate: "2019-04-02", + editorialWeekday: 2, + broadcastTimeJst: "21:54", + broadcastLabel: "TOKYO MX/BS11 每週二 21:54(日本時間;2019 年 TV 短篇)", + status: "finished", + verifiedAt: "2026-09-08", + officialSiteUrl: "https://idolmaster.jp/blog/?p=58824", + identifierSource: { + label: "Bandai Namco 官方:2019 年 TV 版作品識別、首播日期與時段", + url: "https://idolmaster.jp/blog/?p=58033", + language: "ja" + }, + wikipediaUrl: "https://youranimes.tw/bangumi/201904", + sourceReferenceUrls: ["https://youranimes.tw/animes/1922"], + themes: [{ + type: "ED", sequence: 1, + titleJa: "きゅん・きゅん・まっくす", + artistDisplayName: "一ノ瀬志希(CV:藍原ことみ)、乙倉悠貴(CV:中島由貴)、椎名法子(CV:都丸ちよ)、前川みく(CV:高森奈津美)、棟方愛海(CV:藤本彩花)", + releaseDate: "2019-04-17", + versionLabel: "TV 4 月 ED/CD 完整版;官方試聽短版", + credits: [ + { name: "藍原ことみ", role: "vocals" }, + { name: "中島由貴", role: "vocals" }, + { name: "都丸ちよ", role: "vocals" }, + { name: "高森奈津美", role: "vocals" }, + { name: "藤本彩花", role: "vocals" }, + { name: "坂井竜二", role: "lyrics" }, + { name: "BNSI(kyo)", role: "composition" } + ] + }, { + type: "ED", sequence: 2, + titleJa: "Max Beat", + artistDisplayName: "高垣楓(CV:早見沙織)、鷹富士茄子(CV:森下来奈)、二宮飛鳥(CV:青木志貴)、松永涼(CV:千菅春香)、大和亜季(CV:村中知)", + releaseDate: "2019-05-22", + versionLabel: "TV 5 月 ED/CD 完整版;官方試聽短版", + credits: [ + { name: "早見沙織", role: "vocals" }, + { name: "森下来奈", role: "vocals" }, + { name: "青木志貴", role: "vocals" }, + { name: "千菅春香", role: "vocals" }, + { name: "村中知", role: "vocals" }, + { name: "渡部紫緒", role: "lyrics" }, + { name: "坂部剛", role: "composition" }, + { name: "坂部剛", role: "arrangement" } + ] + }, { + type: "ED", sequence: 3, + titleJa: "TAKAMARI☆CLIMAXXX!!!!!", + artistDisplayName: "喜多日菜子(CV:深川芹亜)、喜多見柚(CV:武田羅梨沙多胡)、南条光(CV:神谷早矢佳)、日野茜(CV:赤﨑千夏)、姫川友紀(CV:杜野まこ)", + releaseDate: "2019-06-19", + versionLabel: "TV 6 月 ED/CD 完整版;官方試聽短版", + credits: [ + { name: "深川芹亜", role: "vocals" }, + { name: "武田羅梨沙多胡", role: "vocals" }, + { name: "神谷早矢佳", role: "vocals" }, + { name: "赤﨑千夏", role: "vocals" }, + { name: "杜野まこ", role: "vocals" }, + { name: "広川恵一(MONACA)", role: "lyrics" }, + { name: "広川恵一(MONACA)", role: "composition" }, + { name: "広川恵一(MONACA)", role: "arrangement" } + ] + }] +}, { id: "catalog-youkai-watch-2019", slug: "youkai-watch-2019", titleJa: "妖怪ウォッチ!", diff --git a/src/data/curated-theme-sources/2019/spring.ts b/src/data/curated-theme-sources/2019/spring.ts index 3a441d1..bb03fbe 100644 --- a/src/data/curated-theme-sources/2019/spring.ts +++ b/src/data/curated-theme-sources/2019/spring.ts @@ -3,6 +3,18 @@ import { getCuratedAnimeKey, getCuratedThemeKey } from "@/data/curated-seeds/ide import type { CuratedThemeSourceOverrideMap, CuratedThemeSourceSeed } from "@/data/curated-theme-sources/types"; const firstPartyUrlsByTheme: Readonly> = { + "catalog-cinderella-girls-climax-2019:ED:1": [ + "https://columbia.jp/idolmaster/imasnews/190405.html", + "https://www.youtube.com/watch?v=QkO1DC96kIM" + ], + "catalog-cinderella-girls-climax-2019:ED:2": [ + "https://columbia.jp/idolmaster/imasnews/190510.html", + "https://www.youtube.com/watch?v=bqUcdQSW3qA" + ], + "catalog-cinderella-girls-climax-2019:ED:3": [ + "https://columbia.jp/idolmaster/imasnews/190607.html", + "https://www.youtube.com/watch?v=gBvq0uP4Hos" + ], "catalog-youkai-watch-2019:OP:1": [ "https://maekawakikaku.co.jp/news/archives/2019/20190604_837.html", "https://popholic.jp/archives/12216", @@ -261,6 +273,12 @@ const firstPartyUrlsByTheme: Readonly> = { }; const sourceLabelsByUrl: Readonly> = { + "https://columbia.jp/idolmaster/imasnews/190405.html": "日本 Columbia:TV 4 月 ED、演唱及製作署名、CD 日期與官方試聽", + "https://www.youtube.com/watch?v=QkO1DC96kIM": "日本 Columbia:4 月 ED 官方試聽短版與演唱者", + "https://columbia.jp/idolmaster/imasnews/190510.html": "日本 Columbia:TV 5 月 ED、演唱及製作署名、CD 日期與官方試聽", + "https://www.youtube.com/watch?v=bqUcdQSW3qA": "日本 Columbia:5 月 ED 官方試聽短版與演唱者", + "https://columbia.jp/idolmaster/imasnews/190607.html": "日本 Columbia:TV 6 月 ED、演唱及製作署名、CD 日期與官方試聽", + "https://www.youtube.com/watch?v=gBvq0uP4Hos": "日本 Columbia:6 月 ED 官方試聽短版與演唱者", "https://maekawakikaku.co.jp/news/archives/2019/20190604_837.html": "紘毅所屬事務所:2019 年版 OP、CD 發行日期與短版 MV", "https://popholic.jp/archives/12216": "POPHOLIC 創作者官方:OP 用途、演唱者與菊谷知樹的原曲編曲署名", "https://www.ymm.co.jp/p/detail.php?code=GTK01097308&dm=sl&dso=11&ua=pc": "Yamaha 樂譜出版方:原曲詞曲署名;鋼琴改編及書籍日期不作動畫原曲資料", @@ -388,6 +406,12 @@ const sourceLabelsByUrl: Readonly> = { }; const crossCheckSourcesByAnime: Readonly> = { + "catalog-cinderella-girls-climax-2019": { + label: "アニソン・オンライン:2019 春季、三首 ED 用途與合唱名義交叉核對", + url: "https://anison.online/anime/1069", + language: "ja", + role: "cross_check" + }, "catalog-youkai-watch-2019": { label: "アニソン・オンライン:2019 春季 TV 系列、OP/ED 用途與演唱者交叉核對", url: "https://anison.online/anime/1122", diff --git a/src/data/curated-theme-videos/2019/spring.ts b/src/data/curated-theme-videos/2019/spring.ts index 83d8da0..67de644 100644 --- a/src/data/curated-theme-videos/2019/spring.ts +++ b/src/data/curated-theme-videos/2019/spring.ts @@ -1,6 +1,30 @@ import type { CuratedThemeVideoOverrideMap } from "@/data/curated-theme-videos/types"; export const curated2019SpringThemeVideos = { + "catalog-cinderella-girls-climax-2019:ED:1": [{ + youtubeVideoId: "QkO1DC96kIM", + title: "【アイドルマスター】「きゅん・きゅん・まっくす」(歌:一ノ瀬志希、乙倉悠貴、椎名法子、前川みく、棟方愛海)", + type: "official_audio", + channelName: "日本コロムビア 公式YouTubeチャンネル", + officialStatus: "official", + embeddable: true + }], + "catalog-cinderella-girls-climax-2019:ED:2": [{ + youtubeVideoId: "bqUcdQSW3qA", + title: "【アイドルマスター】「Max Beat」(歌:高垣楓、鷹富士茄子、二宮飛鳥、松永涼、大和亜季)", + type: "official_audio", + channelName: "日本コロムビア 公式YouTubeチャンネル", + officialStatus: "official", + embeddable: true + }], + "catalog-cinderella-girls-climax-2019:ED:3": [{ + youtubeVideoId: "gBvq0uP4Hos", + title: "【アイドルマスター】「TAKAMARI☆CLIMAXXX!!!!!」(歌:喜多日菜子、喜多見柚、南条光、日野茜、姫川友紀)", + type: "official_audio", + channelName: "日本コロムビア 公式YouTubeチャンネル", + officialStatus: "official", + embeddable: true + }], "catalog-youkai-watch-2019:OP:1": [{ youtubeVideoId: "qWcfCWkb9gw", title: "紘毅 / ケラケラホーのうた MUSIC VIDEO (short ver.)", diff --git a/tests/e2e/phase1.spec.ts b/tests/e2e/phase1.spec.ts index b8a1b06..de5e4c7 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("1950"); + await expect(page.locator("[data-catalog-anime-count]")).toHaveText("1951"); 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("1950"); + await expect(page.locator("[data-catalog-anime-count]")).toHaveText("1951"); expect(externalRequests).toEqual([]); }); @@ -908,7 +908,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,950 個作品頁與 4,325 首 OP/ED"); + await expect(page.getByLabel("季度資料狀態")).toContainText("已發布 29 個季度、1,951 個作品頁與 4,328 首 OP/ED"); await page.setViewportSize({ width: 390, height: 844 }); await page.reload(); @@ -1245,10 +1245,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: /春季動畫.*33 套動畫/ }).click(); + await page.getByRole("link", { name: /春季動畫.*34 套動畫/ }).click(); await expect(page.getByRole("heading", { name: "2019 春季動畫" })).toBeVisible(); - await expect(page.getByText("本季正在補充,目前收錄 33 套 TV 作品(含電視短篇及重編版)。其餘作品、跨季延續及特殊歌曲版本仍待核對。", { exact: true })).toBeVisible(); - await expect(page.locator("[data-anime-card]")).toHaveCount(33); + await expect(page.getByText("本季正在補充,目前收錄 34 套 TV 作品(含電視短篇及重編版)。其餘作品、跨季延續及特殊歌曲版本仍待核對。", { exact: true })).toBeVisible(); + await expect(page.locator("[data-anime-card]")).toHaveCount(34); await expect(page.locator("[data-anime-card] img")).toHaveCount(0); expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); @@ -1681,6 +1681,77 @@ test("anime details keep full-width poster-free headings and readable metadata a } }); +test("CLIMAX SEASON search preserves monthly ensemble endings, individual singers and official short previews", async ({ page, browser, request }) => { + test.setTimeout(60_000); + const slug = "cinderella-girls-climax-2019"; + const mediaRequests: string[] = []; + page.on("request", (item) => { + if (/youtube|ytimg|googlevideo/.test(new URL(item.url()).hostname)) mediaRequests.push(item.url()); + }); + const response = await request.get(`/api/v1/anime/${slug}.json`); + expect(response.status()).toBe(200); + const detail = await response.json(); + expect(detail.id).toBe("catalog-cinderella-girls-climax-2019"); + expect(detail.themes.map(({ type }: { type: string }) => type)).toEqual(["ED", "ED", "ED"]); + for (const width of [1280, 960, 390, 320]) { + await page.setViewportSize({ width, height: 844 }); + await page.goto("/search/", { waitUntil: "domcontentloaded" }); + await page.getByRole("searchbox", { name: "搜尋動畫、歌曲或創作者" }).fill("灰姑娘女孩劇場"); + await page.getByLabel("年份", { exact: true }).selectOption("2019"); + await page.getByLabel("季度", { exact: true }).selectOption("spring"); + await expect(page.locator("[data-catalog-result]")).toHaveCount(1); + await page.getByRole("link", { name: /アイドルマスター シンデレラガールズ劇場 CLIMAX SEASON/ }).first().click(); + await expect(page).toHaveURL(new RegExp(`/anime/${slug}/$`)); + await expect(page.locator(".anime-hero__zh")).toHaveText("灰姑娘女孩劇場 第四季:CLIMAX SEASON"); + await expect(page.locator(".anime-hero__romaji")).toHaveCount(0); + await expect(page.locator(".theme-card")).toHaveCount(3); + await expect(page.locator(".theme-navigation")).toHaveCount(0); + await expect(page.locator(".theme-card time")).toHaveText(["2019-04-17", "2019-05-22", "2019-06-19"]); + for (let index = 1; index <= 3; index += 1) { + const theme = page.locator(`#theme-${slug}-ed-${index}`); + await expect(theme).toContainText(`TV ${index + 3} 月 ED/CD 完整版;官方試聽短版`); + await expect(theme.getByRole("button", { name: /載入 YouTube 影片/ })).toHaveCount(1); + } + const may = page.locator(`#theme-${slug}-ed-2`); + await expect(may.locator(".theme-card__credits dd").first()).toHaveText("早見沙織、森下来奈、青木志貴、千菅春香、村中知"); + await may.getByRole("link", { name: "搜尋「森下来奈」的歌曲", exact: true }).focus(); + await page.keyboard.press("Enter"); + await expect(page.getByRole("searchbox", { name: "搜尋動畫、歌曲或創作者" })).toHaveValue("森下来奈"); + for (const [label, value] of [["年份", "2019"], ["季度", "spring"], ["歌曲用途", "ED"]]) await page.getByLabel(label!, { exact: true }).selectOption(value!); + await expect(page.locator("[data-catalog-result]")).toHaveCount(1); + await page.getByRole("link", { name: "Max Beat", exact: true }).click(); + await expect(may).toBeFocused(); + await expect(may.locator("details")).not.toHaveAttribute("open", ""); + await may.locator("details summary").focus(); + await page.keyboard.press("Enter"); + await expect(may.locator('a[href="https://columbia.jp/idolmaster/imasnews/190510.html"]')).toBeVisible(); + 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: "Official audio preview fixture" })); + await page.locator(`#theme-${slug}-ed-2`).getByRole("button", { name: /載入 YouTube 影片/ }).focus(); + await page.keyboard.press("Enter"); + await expect(page.locator("iframe")).toHaveCount(1); + await expect(page.locator("iframe")).toHaveAttribute("src", /youtube-nocookie\.com\/embed\/bqUcdQSW3qA/); + await expect(page.locator(`#theme-${slug}-ed-1 iframe, #theme-${slug}-ed-3 iframe`)).toHaveCount(0); + const context = await browser.newContext({ javaScriptEnabled: false, viewport: { width: 390, height: 844 } }); + try { + const nativePage = await context.newPage(); + await nativePage.goto(`${e2eOrigin}/catalog/2019/`, { waitUntil: "domcontentloaded" }); + await nativePage.getByRole("link", { name: /春季動畫.*34 套動畫/ }).click(); + await nativePage.getByRole("link", { name: /アイドルマスター シンデレラガールズ劇場 CLIMAX SEASON/ }).first().click(); + await expect(nativePage.locator(".theme-card")).toHaveCount(3); + await nativePage.locator(`#theme-${slug}-ed-3 details summary`).focus(); + await nativePage.keyboard.press("Enter"); + await expect(nativePage.locator(`#theme-${slug}-ed-3 a[href="https://columbia.jp/idolmaster/imasnews/190607.html"]`)).toBeVisible(); + await expect(nativePage.locator("iframe")).toHaveCount(0); + expect(await nativePage.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); + } finally { + await context.close(); + } +}); + test("Yo-kai Watch search separates the 2019 series and keeps shared writers and short video choices attached to its songs", async ({ page, browser, request }) => { const slug = "youkai-watch-2019"; const mediaRequests: string[] = []; @@ -1737,7 +1808,7 @@ test("Yo-kai Watch search separates the 2019 series and keeps shared writers and try { const nativePage = await context.newPage(); await nativePage.goto(`${e2eOrigin}/catalog/2019/`); - await nativePage.getByRole("link", { name: /春季動畫.*33 套動畫/ }).click(); + await nativePage.getByRole("link", { name: /春季動畫.*34 套動畫/ }).click(); await nativePage.getByRole("link", { name: /妖怪ウォッチ!/ }).first().click(); const sources = nativePage.locator(`#theme-${slug}-ed-1 details`); await sources.locator("summary").focus(); @@ -1794,7 +1865,7 @@ test("Bakugan Japanese theme search preserves shared credits and separates the l try { const nativePage = await context.newPage(); await nativePage.goto(`${e2eOrigin}/catalog/2019/`); - await nativePage.getByRole("link", { name: /春季動畫.*33 套動畫/ }).click(); + await nativePage.getByRole("link", { name: /春季動畫.*34 套動畫/ }).click(); await nativePage.getByRole("link", { name: /爆丸バトルプラネット/ }).first().click(); await expect(nativePage).toHaveURL(new RegExp(`/anime/${slug}/$`)); await expect(nativePage.locator(".theme-card")).toHaveCount(3); @@ -1935,7 +2006,7 @@ test("501 Takeoff creator search reaches the correct solo ending and keeps the g try { const nativePage = await context.newPage(); await nativePage.goto(`${e2eOrigin}/catalog/2019/`); - await nativePage.getByRole("link", { name: /春季動畫.*33 套動畫/ }).click(); + await nativePage.getByRole("link", { name: /春季動畫.*34 套動畫/ }).click(); await nativePage.getByRole("link", { name: /ストライクウィッチーズ 501部隊発進しますっ/ }).first().click(); await expect(nativePage).toHaveURL(new RegExp(`/anime/${slug}/$`)); await expect(nativePage.locator(".theme-card")).toHaveCount(13); @@ -1997,7 +2068,7 @@ test("Gonjiro search preserves independent identity, co-composers and the licens const context = await browser.newContext({ javaScriptEnabled: false, viewport: { width: 390, height: 844 } }); const nativePage = await context.newPage(); await nativePage.goto(`${e2eOrigin}/catalog/2019/`); - await nativePage.getByRole("link", { name: /春季動畫.*33 套動畫/ }).click(); + await nativePage.getByRole("link", { name: /春季動畫.*34 套動畫/ }).click(); await nativePage.getByRole("link", { name: /けだまのゴンじろー/ }).first().click(); await expect(nativePage).toHaveURL(/\/anime\/gonjiro-2019\/$/); await expect(nativePage.locator(".theme-card")).toHaveCount(2); @@ -2295,8 +2366,8 @@ 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("22"); - const expectedCounts = [3, 4, 1, 1, 4, 4, 5]; + await expect(page.locator("[data-result-count]")).toHaveText("23"); + const expectedCounts = [3, 5, 1, 1, 4, 4, 5]; for (const [index, link] of (await navigation.getByRole("link").all()).entries()) { const target = page.locator((await link.getAttribute("href"))!); await expect(target).toBeVisible(); @@ -2307,7 +2378,7 @@ test("weekday navigation follows populated groups and remains useful after filte 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("33"); + await expect(page.locator("[data-result-count]")).toHaveText("34"); await expect(page.getByRole("checkbox", { name: "有 OP" })).toBeFocused(); expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); } @@ -2330,7 +2401,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(33); + await expect(nativePage.locator("[data-anime-card]")).toHaveCount(34); 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 292e71a..5add376 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: 1950 + animeCount: 1951 }); }); diff --git a/tests/unit/catalog-navigation.test.ts b/tests/unit/catalog-navigation.test.ts index 6596b05..5d12d9d 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(1950); + expect(searchCatalog(index, defaults)).toHaveLength(1951); }); 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 85f1764..cf5c9c6 100644 --- a/tests/unit/curated-2019-spring.test.ts +++ b/tests/unit/curated-2019-spring.test.ts @@ -10,6 +10,85 @@ 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 CLIMAX SEASON as the 2019 TV quarter separately from Extra Stage and game-only shorts", async () => { + const seed = curated2019SpringSeeds.find(({ id }) => id === "catalog-cinderella-girls-climax-2019")!; + expect(seed).toMatchObject({ startDate: "2019-04-02", editorialWeekday: 2, broadcastTimeJst: "21:54", seasonIds: ["2019-spring"] }); + const detail = curatedAnimeDetails.find(({ id }) => id === seed.id)!; + expect(detail).toMatchObject({ slug: "cinderella-girls-climax-2019", titleJa: "アイドルマスター シンデレラガールズ劇場 CLIMAX SEASON", titleZhHant: "灰姑娘女孩劇場 第四季:CLIMAX SEASON", status: "finished" }); + for (const omitted of ["anilistId", "anilistUrl", "titleRomaji", "posterUrl", "bannerUrl"]) { + expect(seed).not.toHaveProperty(omitted); + expect(detail).not.toHaveProperty(omitted); + } + expect(detail.sources).toContainEqual(expect.objectContaining({ url: "https://idolmaster.jp/blog/?p=58033", role: "identifier", language: "ja" })); + expect(detail.sources).toContainEqual(expect.objectContaining({ url: "https://youranimes.tw/bangumi/201904", role: "localized_cross_check", language: "zh-Hant" })); + const extra = anime(115519); + expect(extra.slug).toBe("cinderella-girls-gekijou-extra-stage"); + expect(extra.id).not.toBe(detail.id); + expect(extra.themes.some(({ titleJa }) => detail.themes.some((theme) => theme.titleJa === titleJa))).toBe(false); + 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("keeps CLIMAX SEASON's three monthly ensemble endings distinct from CD solos and bonus tracks", () => { + const themes = curatedAnimeDetails.find(({ id }) => id === "catalog-cinderella-girls-climax-2019")!.themes; + expect(themes.map(({ type, sequence, titleJa, releaseDate }) => [type, sequence, titleJa, releaseDate])).toEqual([ + ["ED", 1, "きゅん・きゅん・まっくす", "2019-04-17"], + ["ED", 2, "Max Beat", "2019-05-22"], + ["ED", 3, "TAKAMARI☆CLIMAXXX!!!!!", "2019-06-19"] + ]); + const singers = [ + ["藍原ことみ", "中島由貴", "都丸ちよ", "高森奈津美", "藤本彩花"], + ["早見沙織", "森下来奈", "青木志貴", "千菅春香", "村中知"], + ["深川芹亜", "武田羅梨沙多胡", "神谷早矢佳", "赤﨑千夏", "杜野まこ"] + ]; + for (const [index, theme] of themes.entries()) { + expect(theme.versionLabel).toContain(`TV ${index + 4} 月 ED`); + expect(theme.versionLabel).toContain("CD 完整版;官方試聽短版"); + expect(theme.credits.filter(({ role }) => role === "vocals").map(({ name }) => name)).toEqual(singers[index]); + for (const singer of singers[index]!) expect(theme.artistDisplayName).toContain(`CV:${singer}`); + } + expect(themes[0]?.credits.filter(({ role }) => role !== "vocals")).toEqual([ + { name: "坂井竜二", role: "lyrics" }, { name: "BNSI(kyo)", role: "composition" } + ]); + expect(themes[1]?.credits.filter(({ role }) => role !== "vocals")).toEqual([ + { name: "渡部紫緒", role: "lyrics" }, { name: "坂部剛", role: "composition" }, { name: "坂部剛", role: "arrangement" } + ]); + expect(themes[2]?.credits.filter(({ role }) => role !== "vocals")).toEqual([ + { name: "広川恵一(MONACA)", role: "lyrics" }, { name: "広川恵一(MONACA)", role: "composition" }, { name: "広川恵一(MONACA)", role: "arrangement" } + ]); + expect(themes.flatMap(({ credits }) => credits).some(({ name }) => ["天野聡美", "原田彩楓", "高田憂希"].includes(name))).toBe(false); + }); + + it("retains dated CLIMAX SEASON source evidence and official audio previews without treating them as full videos", () => { + const themes = curatedAnimeDetails.find(({ id }) => id === "catalog-cinderella-girls-climax-2019")!.themes; + const news = ["190405", "190510", "190607"]; + const videos = ["QkO1DC96kIM", "bqUcdQSW3qA", "gBvq0uP4Hos"]; + for (const [index, theme] of themes.entries()) { + expect(theme.lastVerifiedAt).toBe("2026-09-08"); + expect(theme.sources.every(({ verifiedAt }) => verifiedAt === "2026-09-08")).toBe(true); + expect(theme.sources).toContainEqual(expect.objectContaining({ url: `https://columbia.jp/idolmaster/imasnews/${news[index]}.html`, role: "first_party", language: "ja" })); + expect(theme.sources).toContainEqual(expect.objectContaining({ url: "https://anison.online/anime/1069", role: "cross_check", language: "ja" })); + expect(theme.videos).toHaveLength(1); + expect(theme.videos[0]).toMatchObject({ youtubeVideoId: videos[index], type: "official_audio", channelName: "日本コロムビア 公式YouTubeチャンネル", officialStatus: "official", embeddable: true }); + expect(theme.videos[0]?.title).toContain(theme.titleJa); + } + }); + + it("finds CLIMAX SEASON by Chinese title, individual vocalists and exact song writers in its own quarter", async () => { + const index = buildCatalogSearchIndex((await loadCatalogSearchData(new CuratedProvider(), true)).entries); + const options = { year: "2019", quarter: "spring", type: "ED" } as const; + const titleResults = searchCatalog(index, { ...options, query: "灰姑娘女孩劇場", scope: "anime" }); + expect(titleResults.map(({ anime: item }) => item.slug)).toEqual(["cinderella-girls-climax-2019"]); + for (const [query, title] of [["藤本彩花", "きゅん・きゅん・まっくす"], ["森下来奈", "Max Beat"], ["広川恵一(MONACA)", "TAKAMARI☆CLIMAXXX!!!!!"]]) { + const results = searchCatalog(index, { ...options, query: query!, scope: "creators" }); + expect(results.map(({ anime: item }) => item.slug)).toEqual(["cinderella-girls-climax-2019"]); + expect(results[0]?.themes.map(({ titleJa }) => titleJa)).toEqual([title]); + } + expect(searchCatalog(index, { ...options, query: "TAKAMARI☆CLIMAXXX!!!!!", scope: "songs", type: "OP" })).toEqual([]); + }); + it("identifies the 2019 Yo-kai Watch TV series independently from the 2021 musical-note sequel", async () => { const seed = curated2019SpringSeeds.find(({ id }) => id === "catalog-youkai-watch-2019")!; expect(seed).toMatchObject({ startDate: "2019-04-05", editorialWeekday: 5, broadcastTimeJst: "18:25", seasonIds: ["2019-spring"] }); @@ -740,9 +819,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(33); + expect(spring.anime).toHaveLength(34); expect(spring.coverageNote).toContain("跨季延續及特殊歌曲版本仍待核對"); - expect(curated2019SpringSeeds.flatMap(({ themes }) => themes)).toHaveLength(96); + expect(curated2019SpringSeeds.flatMap(({ themes }) => themes)).toHaveLength(99); expect(curatedSeasonDetails.some(({ id }) => id === "2025-fall")).toBe(false); for (const seed of curated2019SpringSeeds) { expect(seed.seasonIds).toEqual(["2019-spring"]); @@ -751,12 +830,12 @@ describe("2019 spring reviewed TV catalogue", () => { expect(detail.bannerUrl).toBeUndefined(); expect(detail.imageSourceUrl).toBeUndefined(); expect(detail.sources).toContainEqual(expect.objectContaining({ - role: "identifier", url: seed.identifierSource?.url, verifiedAt: "2026-09-07" + role: "identifier", url: seed.identifierSource?.url, verifiedAt: seed.verifiedAt })); for (const theme of detail.themes) { expect(theme.sources.some(({ role }) => role === "first_party")).toBe(true); expect(theme.sources.some(({ role }) => role === "cross_check")).toBe(true); - expect(theme.sources.every(({ verifiedAt }) => verifiedAt === "2026-09-07")).toBe(true); + expect(theme.sources.every(({ verifiedAt }) => verifiedAt === seed.verifiedAt)).toBe(true); } } }); diff --git a/tests/unit/curated-catalog.test.ts b/tests/unit/curated-catalog.test.ts index 573d5ca..a8d3a5d 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, 33]); + 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, 34]); expect(curatedSeasonDetails.map((season) => [season.id, season.reviewState, season.verifiedAt])).toEqual([ ["2026-summer", "reviewed", "2026-08-02"], ["2026-spring", "reviewed", "2026-08-02"], @@ -68,12 +68,12 @@ describe("curated public catalogue", () => { ["2020-winter", "reviewed", "2026-09-02"], ["2019-fall", "reviewed", "2026-09-02"], ["2019-summer", "reviewed", "2026-09-07"], - ["2019-spring", "reviewed", "2026-09-07"] + ["2019-spring", "reviewed", "2026-09-08"] ]); - expect(curatedAnimeDetails).toHaveLength(1950); - expect(curatedAnimeDetails.filter((anime) => anime.themes.length > 0)).toHaveLength(1586); - expect(curatedAnimeDetails.flatMap((anime) => anime.themes)).toHaveLength(4325); - expect(curatedAnimeDetails.filter((anime) => anime.themeAvailability === "documented")).toHaveLength(1586); + expect(curatedAnimeDetails).toHaveLength(1951); + expect(curatedAnimeDetails.filter((anime) => anime.themes.length > 0)).toHaveLength(1587); + expect(curatedAnimeDetails.flatMap((anime) => anime.themes)).toHaveLength(4328); + expect(curatedAnimeDetails.filter((anime) => anime.themeAvailability === "documented")).toHaveLength(1587); expect(curatedAnimeDetails.filter((anime) => anime.themeAvailability === "not_used")).toHaveLength(2); expect(curatedAnimeDetails.filter((anime) => anime.themeAvailability === "not_announced")).toHaveLength(362); const youtubeLinks = curatedAnimeDetails @@ -2178,7 +2178,9 @@ describe("curated public catalogue", () => { } for (const item of anime.themes) { - const expectedVerifiedAt = anime.id === "curated-111048" + const expectedVerifiedAt = anime.id === "catalog-cinderella-girls-climax-2019" + ? "2026-09-08" + : anime.id === "curated-111048" || curatedSeasonDetails.find(({ id }) => id === "2019-summer")?.anime.some(({ id }) => id === anime.id) || curatedSeasonDetails.find(({ id }) => id === "2019-spring")?.anime.some(({ id }) => id === anime.id) ? "2026-09-07" diff --git a/tests/unit/curated-season-registry.test.ts b/tests/unit/curated-season-registry.test.ts index 582a74b..b663f68 100644 --- a/tests/unit/curated-season-registry.test.ts +++ b/tests/unit/curated-season-registry.test.ts @@ -40,7 +40,7 @@ describe("curated season registry", () => { ]); const ownedSeeds = curatedSeasonRegistry.flatMap(({ seeds }) => seeds); - expect(ownedSeeds).toHaveLength(1950); + expect(ownedSeeds).toHaveLength(1951); expect(new Set(ownedSeeds.map(getCuratedAnimeKey)).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 d96f514..c610487 100644 --- a/tests/unit/curated-theme-source-registry.test.ts +++ b/tests/unit/curated-theme-source-registry.test.ts @@ -70,11 +70,11 @@ describe("curated theme source registry", () => { 180, 161, 103, - 96 + 99 ]); const keys = curatedThemeSourceRegistry.flatMap(({ overrides }) => Object.keys(overrides)); - expect(keys).toHaveLength(2063); + expect(keys).toHaveLength(2066); 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 33d1319..a3b8fa4 100644 --- a/tests/unit/curated-theme-video-registry.test.ts +++ b/tests/unit/curated-theme-video-registry.test.ts @@ -48,15 +48,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, 49, + 28, 65, 66, 53, 62, 52, 102, 43, 62, 11, 52, ]); const entries = curatedThemeVideoRegistry.flatMap(({ overrides }) => Object.entries(overrides), ); - expect(entries).toHaveLength(1623); + expect(entries).toHaveLength(1626); expect(new Set(entries.map(([key]) => key)).size).toBe(entries.length); - expect(entries.flatMap(([, videos]) => videos)).toHaveLength(1808); + expect(entries.flatMap(([, videos]) => videos)).toHaveLength(1811); expect(Object.keys(curatedThemeVideoOverrides)).toHaveLength( entries.length, ); diff --git a/tests/unit/page-data.test.ts b/tests/unit/page-data.test.ts index 632250c..f898cdd 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(1950); - expect(new Set(result.entries.map(({ anime }) => anime.slug)).size).toBe(1950); - expect(result.entries.filter(({ anime }) => anime.themes.length > 0)).toHaveLength(1586); + expect(result.entries).toHaveLength(1951); + expect(new Set(result.entries.map(({ anime }) => anime.slug)).size).toBe(1951); + expect(result.entries.filter(({ anime }) => anime.themes.length > 0)).toHaveLength(1587); }); it("returns a safe public error state without exposing provider details", async () => {