From 7a1b3894b086a27974babf97d7a983712329fcb2 Mon Sep 17 00:00:00 2001 From: kyeunga25 <49793173+kyeunga25@users.noreply.github.com> Date: Tue, 8 Sep 2026 02:42:38 +0800 Subject: [PATCH] (feat): add 2019 spring KING OF PRISM themes --- CHANGELOG.md | 6 + README.md | 6 +- docs/DATA_PROVENANCE.md | 4 +- docs/DATA_SOURCES.md | 4 +- docs/PROJECT_PLAN.md | 5 +- docs/QA_V1_52_2019_SPRING_KING_OF_PRISM.md | 28 ++ package-lock.json | 4 +- package.json | 2 +- src/data/curated-data.ts | 11 + src/data/curated-season-registry.ts | 2 +- src/data/curated-seeds/2019/spring.ts | 241 ++++++++++++++++++ src/data/curated-theme-sources/2019/spring.ts | 35 +++ tests/e2e/phase1.spec.ts | 112 +++++++- tests/unit/api-smoke.test.ts | 2 +- tests/unit/catalog-navigation.test.ts | 2 +- tests/unit/curated-2019-spring.test.ts | 74 +++++- tests/unit/curated-catalog.test.ts | 12 +- tests/unit/curated-season-registry.test.ts | 2 +- .../curated-theme-source-registry.test.ts | 4 +- tests/unit/page-data.test.ts | 6 +- 20 files changed, 519 insertions(+), 43 deletions(-) create mode 100644 docs/QA_V1_52_2019_SPRING_KING_OF_PRISM.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b990e4..69b299c 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.52.0 — 2019 春季《星光王子》TV 歌曲 + +- 收錄《KING OF PRISM -Shiny Seven Stars-》TV 正篇、OP〈Shiny Seven Stars!〉及十一首 TRF 翻唱 ED,保留七人合唱與角色歌唱者。 +- 分清介紹特別節目、TV 正篇與劇場編輯版;〈JOY〉保留小林竜之的歌唱署名,角色插曲不作 ED。未核對製作署名及〈BOY MEETS GIRL〉有衝突的發行日期維持空白,CD 廣告只作來源證據。 +- 春季共 36 套作品、113 筆 OP/ED;本季與夏季仍在補充,2025 秋季尚未收錄。 + ## v1.51.0 — 2019 春季《叛逆性百萬亞瑟王 第二季》 - 收錄第二季 TV 版的 OP〈OPEN THE WORLDS〉及 ED〈PEARLY×PARTY〉,保留原演唱組合、CD 日期及 OP 的先行配信日期。 diff --git a/README.md b/README.md index 5c06b2e..af786b8 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,952 unique titles and 4,330 known OP/ED records. The 2019 spring snapshot contains thirty-five 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,953 unique titles and 4,342 known OP/ED records. The 2019 spring snapshot contains thirty-six 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.51.0**。新增 2019 春季《叛逆性百萬亞瑟王 第二季》的 OP/ED 與官方片頭 MV,區分原演唱組合、改編版及配信/CD 日期。目錄共有 29 個季度、1,952 套作品與 4,330 筆 OP/ED;春季為 35 套作品、101 筆歌曲,春、夏季仍在補充,2025 秋季尚未收錄。 +目前 source 版本:**v1.52.0**。新增 2019 春季《星光王子 KING OF PRISM -Shiny Seven Stars-》TV 正篇的 OP 與十一首輪替 ED,保留七人合唱及個別歌唱者,區分劇場版、角色插曲與翻唱版本。目錄共有 29 個季度、1,953 套作品與 4,342 筆 OP/ED;春季為 36 套作品、113 筆歌曲,春、夏季仍在補充,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.51.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) | +| 公開靜態目錄 / Public static catalogue | Source 版本為 `v1.52.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 2a81d25..730c137 100644 --- a/docs/DATA_PROVENANCE.md +++ b/docs/DATA_PROVENANCE.md @@ -31,9 +31,9 @@ API、資料庫、商標或媒體重用權。完整授權邊界見 `first_party` 表示動畫製作/發行方、唱片公司、藝人官方渠道、官方影片或正式播出機構的公開頁面。`cross_check` 表示用於發現缺口及交叉比對的公開索引。兩種角色都是來源分類,不代表數值信心或內容評分。 -2019 春季目前整理 35 套 TV 作品(含電視短篇及重編版),夏季整理 38 套 TV 作品及 4 套網絡連載;兩季的其他作品、跨季延續與特殊歌曲仍待核對;`coverageNote` 會在季度頁及 API 公開說明本季範圍。 +2019 春季目前整理 36 套 TV 作品(含電視短篇及重編版),夏季整理 38 套 TV 作品及 4 套網絡連載;兩季的其他作品、跨季延續與特殊歌曲仍待核對;`coverageNote` 會在季度頁及 API 公開說明本季範圍。 -目前二十九季快照的 season references、1,952 個作品及 4,330 筆 OP/ED 都有結構化 provenance;4,330 筆歌曲全部至少保留一個 `first_party` 與一個 `cross_check` 來源。`tests/unit/curated-catalog.test.ts` 鎖定來源數量、角色、語言、HTTPS、核對日期、Mock URL 禁止條件,以及 legacy labels 與結構化 ledger 的一致性。 +目前二十九季快照的 season references、1,953 個作品及 4,342 筆 OP/ED 都有結構化 provenance;4,342 筆歌曲全部至少保留一個 `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 c384314..2dc3e06 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 春季 35 套 TV 作品(含電視短篇及重編版),共 1,952 個作品詳情、4,330 筆 OP/ED 與 567 個歌曲層 YouTube 直連;其中 1,812 筆已保留官方或正式授權影片 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 春季 36 套 TV 作品(含電視短篇及重編版),共 1,953 個作品詳情、4,342 筆 OP/ED 與 567 個歌曲層 YouTube 直連;其中 1,812 筆已保留官方或正式授權影片 metadata。 每季先以繁中年度動畫表的對應月份建立日曆清單,再與 Annict、Bangumi 及 AniList identifier 取聯集核對。OP/ED 以作品官網和官方 YouTube 為主,並用 AnimeThemes、UZUREA 與 UtaTen 等季度主題曲表交叉確認;季度歌曲索引未收錄的作品,須返回 Netflix、唱片公司、IP 官網、正式播出機構或製作公司等第一方頁面補證。台港正式代理、串流或出版社名稱優先;未有可確認的繁中名稱時保留原文,不宣稱為正式授權譯名。 2019 春、夏季仍在補充,2025 秋季尚未收錄;季度快照代表已核對範圍,不代表歷史作品或歌曲已全部補齊。 -每首已發布歌曲在 `PublicTheme.sources[]` 留下 URL、語言、`first_party`/`cross_check` 角色及核對日期。二十九季快照的 4,330 筆歌曲都必須同時具備兩種角色;這是公開 provenance gate,不是 confidence score。第三方索引將歌曲標為 OP/ED、但第一方頁面只證實插曲、劇中歌、Victory Dance、其他節目用途、舊季度歌曲、其他改編版本或一般「主題歌」時,不把它升格為官方 OP/ED;在既有導航契約中保留的一般主題曲會明示「主題歌(官方分類)」。完整欄位與媒體邊界見 [`DATA_PROVENANCE.md`](./DATA_PROVENANCE.md)。 +每首已發布歌曲在 `PublicTheme.sources[]` 留下 URL、語言、`first_party`/`cross_check` 角色及核對日期。二十九季快照的 4,342 筆歌曲都必須同時具備兩種角色;這是公開 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 da8cd5d..9b09175 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 春季 35 套非成人 TV/網絡連載作品; -- 跨季作品共用詳情頁,共 1,952 個唯一作品及 4,330 筆已審閱 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 春季 36 套非成人 TV/網絡連載作品; +- 跨季作品共用詳情頁,共 1,953 個唯一作品及 4,342 筆已審閱 OP/ED; - 五個固定導覽入口、年代/年份/季度目錄、星期一至星期日及不定期分組、分頁本機搜尋、動畫詳情與同源 static JSON API; - 每個季度、作品及歌曲公開來源、語言、review state 及核對日期;每首歌曲同時具備第一方/交叉核對來源; - 官方/licensed YouTube 導航、明確第三方連線同意、來源歸屬及無圖 fallback; @@ -86,6 +86,7 @@ - **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。 - **v1.51.0 source**:2019 春季增加《叛逆性百萬亞瑟王 第二季》、2 筆 OP/ED 及 1 筆官方片頭 MV metadata;區分原演唱組合、改編版與配信/CD 日期,未核對的個別演唱及製作署名維持空白。春季共 35 套作品與 101 筆歌曲;發布證據見對應 GitHub Release。 +- **v1.52.0 source**:2019 春季增加《星光王子 KING OF PRISM -Shiny Seven Stars-》TV 正篇與 12 筆 OP/ED,保留七人合唱及個別歌唱者;介紹特別節目、劇場片尾、角色插曲及其他演唱版本保持獨立,衝突日期與未核對製作署名留空。春季共 36 套作品與 113 筆歌曲;發布證據見對應 GitHub Release。 - 正式網站使用 custom domain;非正式 Cloudflare hostname 不在公開文檔記錄; - 私有 backend、資料庫、crawler、帳戶設定、憑證與內部規則不屬於本公開 repository。 diff --git a/docs/QA_V1_52_2019_SPRING_KING_OF_PRISM.md b/docs/QA_V1_52_2019_SPRING_KING_OF_PRISM.md new file mode 100644 index 0000000..b6d01c3 --- /dev/null +++ b/docs/QA_V1_52_2019_SPRING_KING_OF_PRISM.md @@ -0,0 +1,28 @@ +# v1.52.0 QA:2019 春季《星光王子》TV 歌曲 + +來源核對日期:2026-09-08。本切片新增《KING OF PRISM -Shiny Seven Stars-》TV 正篇與 12 筆 OP/ED。春季共 36 套作品、113 筆歌曲;春、夏季仍在補充,2025 秋季尚未收錄。 + +## 資料與來源 + +- [繁中季度清單](https://youranimes.tw/bangumi/201904)、[YourAnimes 個別條目](https://youranimes.tw/animes/488)與 [LINE TV 正式播出頁](https://www.linetv.tw/drama/18245/eps/7)核對春季 TV 版、繁中名稱及 12 話完結狀態。 +- [東京電視台第 1 話節目頁](https://www.tv-tokyo.co.jp/broad_tvtokyo/program/detail/201904/25383_201904152535.html)確認日本編輯播出日為 2019-04-15、每週一 25:35(翌日 01:35)。[官方初回公告](https://kinpri.com/sss/sp/news/detail.php?artist_cd=KOP3S&id=1071169)說明 4 月 8 日播出的是介紹特別節目;劇場編輯版及介紹節目不另計為本季 TV 正篇。 +- [官方 OP 商品頁](https://kinpri.com/sss/sp/discography/detail.php?id=1016430)與播出頁確認〈Shiny Seven Stars!〉的 OP 用途、七人演唱名義及 2019-04-24 CD 日期;保留各位歌唱者。 +- 十一首輪替 ED 分別核對角色單曲商品頁,以及官方 CD 廣告第 [1](https://www.youtube.com/watch?v=420RJfS0G7Y)、[2](https://www.youtube.com/watch?v=lmIgYf6WehM)、[3](https://www.youtube.com/watch?v=lp494sLN5Aw)、[4](https://www.youtube.com/watch?v=lCI1f13Vvw4) 批說明中的 TV 輪替 ED 用途;以[歌曲索引](https://anison.online/anime/4114)交叉確認。個別集數未逐集取得第一方證據,歌曲排序不宣稱為集數。 +- [〈JOY〉官方單曲](https://kinpri.com/sss/sp/discography/detail.php?id=1016533)記載歌唱名義為高田馬場ジョージGS(CV.小林竜之);搜尋保留這項歌唱身份。各翻唱版的詞曲編曲尚待第一方核對。 +- [〈BOY MEETS GIRL〉商品頁](https://kinpri.com/sss/sp/discography/detail.php?id=1016540)列 CD 日期 2019-08-07,但官方第 4 批 CM 說明對該張 CD 寫作 2019-08-17,未取得官方更正說明,因此此曲 `releaseDate` 留空並顯示待核對。其餘十首 ED 保留各商品頁一致的 CD 日期。 +- 〈366LOVEダイアリー〉為劇場版片尾;角色插曲及其他演唱版本不作 TV ED,包括〈survival dAnce~no no cry more~〉、〈ナナイロノチカイ! -Brilliant oath-〉與〈プラトニックソード〉。 +- 四支官方 CD 廣告均為 30 秒,只用公開文字說明核對用途。本切片不將 CD 廣告當作歌曲播放 metadata,也不下載媒體、圖片、歌詞或複製來源頁面。 + +## 本機驗收 + +- `npm run catalog:check`:248 項通過;`npm test`:351 項通過。 +- `npm run check` 的公開邊界、完整依賴稽核、lint、unit、build 及 Cloudflare dry-run 通過;Chromium 首次因本機執行限制無法啟動,改在允許的環境執行。新測試修正來源收合狀態及季度卡片的無障礙名稱後,完整 `npm run test:e2e -- --workers=1`:51 項通過。 +- 完整與正式依賴稽核均為 0 項漏洞;`fast-uri` 維持 3.1.6,既有 8 項安全回歸包含在 unit suite。 +- 建置 1,997 份 HTML,3,993 份產物通過公開邊界;Cloudflare dry-run 沒有 runtime binding。 +- 對比上一個 source 切片,1,981 份既有 API 完全相同,只更新春季清單並新增一份作品詳情 API;沒有移除。 +- Browser plugin 未提供,使用既有 Playwright。檢查 1280、960、390、320px 及 390px 無 JavaScript,共 30 個畫面;沒有水平溢出、framework overlay、page error、預先 YouTube 請求或 iframe。已目視桌面與手機的七人演唱者換行、收合歌曲導覽及未核對日期狀態。 +- 操作涵蓋繁中名稱/個別歌唱者搜尋、年份與季度/OP/ED 篩選、歌曲錨點焦點、Enter 展開來源,以及無 JavaScript 的年份 → 季度 → 作品 → 第十一首 ED。完整回歸亦涵蓋鍵盤、返回、分頁、離線、404、影片同意與錯誤頁。 + +## 發布界線 + +本機與分支建置結果不代表正式網站已更新。發布時仍須核對合併 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 d8980cc..2d20eb1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "anisonary", - "version": "1.51.0", + "version": "1.52.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "anisonary", - "version": "1.51.0", + "version": "1.52.0", "license": "SEE LICENSE IN LICENSING.md", "dependencies": { "@astrojs/sitemap": "^3.7.3", diff --git a/package.json b/package.json index 5978b92..f5805cc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "anisonary", - "version": "1.51.0", + "version": "1.52.0", "private": true, "license": "SEE LICENSE IN LICENSING.md", "type": "module", diff --git a/src/data/curated-data.ts b/src/data/curated-data.ts index 7d05756..c7dc5ed 100644 --- a/src/data/curated-data.ts +++ b/src/data/curated-data.ts @@ -4610,6 +4610,17 @@ const animeOfficialSourceOverrides: Record = { + "catalog-king-of-prism-sss-2019": [{ + label: "LINE TV 正式播出頁:繁中 TV 名稱與完結集數", + url: "https://www.linetv.tw/drama/18245/eps/7", + language: "zh-Hant", + role: "first_party" + }, { + label: "動畫官方:4 月 8 日介紹特別節目與 TV 正篇的分界", + url: "https://kinpri.com/sss/sp/news/detail.php?artist_cd=KOP3S&id=1071169", + language: "ja", + role: "first_party" + }], "catalog-bakugan-battle-planet-2019": [{ label: "YOYOTV 台灣播出方:繁中作品名稱;中文主題影片不作日語 OP/ED 版本", url: "https://www.youtube.com/watch?v=9yiBbt820uo", diff --git a/src/data/curated-season-registry.ts b/src/data/curated-season-registry.ts index 17dfe80..a5864c6 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: "本季正在補充,目前收錄 35 套 TV 作品(含電視短篇及重編版)。其餘作品、跨季延續及特殊歌曲版本仍待核對。", + coverageNote: "本季正在補充,目前收錄 36 套 TV 作品(含電視短篇及重編版)。其餘作品、跨季延續及特殊歌曲版本仍待核對。", seeds: curated2019SpringSeeds, animeIds: curated2019SpringAnimeIds } diff --git a/src/data/curated-seeds/2019/spring.ts b/src/data/curated-seeds/2019/spring.ts index 240e915..1d3edc5 100644 --- a/src/data/curated-seeds/2019/spring.ts +++ b/src/data/curated-seeds/2019/spring.ts @@ -1698,6 +1698,247 @@ const strikeWitches501EndingRows = [ ] as const; const independentSpringSeeds: NativeCuratedAnimeSeed[] = [{ + "id": "catalog-king-of-prism-sss-2019", + "slug": "king-of-prism-sss-2019", + "titleJa": "KING OF PRISM -Shiny Seven Stars-", + "titleZhHant": "星光王子 KING OF PRISM -Shiny Seven Stars-", + "seasonIds": [ + "2019-spring" + ], + "startDate": "2019-04-15", + "editorialWeekday": 1, + "broadcastTimeJst": "25:35", + "broadcastLabel": "東京電視台每週一 25:35(日本編輯播出日;正篇第 1 話,翌日 01:35)", + "status": "finished", + "verifiedAt": "2026-09-08", + "officialSiteUrl": "https://kinpri.com/sss/", + "identifierSource": { + "label": "東京電視台:TV 正篇第 1 話識別、首播日期與深夜時段", + "url": "https://www.tv-tokyo.co.jp/broad_tvtokyo/program/detail/201904/25383_201904152535.html", + "language": "ja" + }, + "wikipediaUrl": "https://youranimes.tw/bangumi/201904", + "sourceReferenceUrls": [ + "https://youranimes.tw/animes/488" + ], + "themes": [ + { + "type": "OP", + "sequence": 1, + "titleJa": "Shiny Seven Stars!", + "artistDisplayName": "一条シン(CV:寺島惇太)、太刀花ユキノジョウ(CV:斉藤壮馬)、香賀美タイガ(CV:畠中祐)、十王院カケル(CV:八代拓)、鷹梁ミナト(CV:五十嵐雅)、西園寺レオ(CV:永塚拓馬)、涼野ユウ(CV:内田雄馬)", + "releaseDate": "2019-04-24", + "versionLabel": "TV OP/七人合唱版;製作署名待核對", + "credits": [ + { + "name": "寺島惇太", + "role": "vocals" + }, + { + "name": "斉藤壮馬", + "role": "vocals" + }, + { + "name": "畠中祐", + "role": "vocals" + }, + { + "name": "八代拓", + "role": "vocals" + }, + { + "name": "五十嵐雅", + "role": "vocals" + }, + { + "name": "永塚拓馬", + "role": "vocals" + }, + { + "name": "内田雄馬", + "role": "vocals" + } + ] + }, + { + "type": "ED", + "sequence": 1, + "titleJa": "寒い夜だから・・・", + "artistDisplayName": "太刀花ユキノジョウ(CV:斉藤壮馬)", + "releaseDate": "2019-06-26", + "versionLabel": "TV 輪替 ED/TRF 翻唱;製作署名待核對", + "credits": [ + { + "name": "斉藤壮馬", + "role": "vocals" + } + ] + }, + { + "type": "ED", + "sequence": 2, + "titleJa": "masquerade", + "artistDisplayName": "香賀美タイガ(CV:畠中祐)", + "releaseDate": "2019-06-26", + "versionLabel": "TV 輪替 ED/TRF 翻唱;製作署名待核對", + "credits": [ + { + "name": "畠中祐", + "role": "vocals" + } + ] + }, + { + "type": "ED", + "sequence": 3, + "titleJa": "Unite! The Night!", + "artistDisplayName": "十王院カケル(CV:八代拓)", + "releaseDate": "2019-07-10", + "versionLabel": "TV 輪替 ED/TRF 翻唱;製作署名待核對", + "credits": [ + { + "name": "八代拓", + "role": "vocals" + } + ] + }, + { + "type": "ED", + "sequence": 4, + "titleJa": "JOY", + "artistDisplayName": "高田馬場ジョージGS(CV:小林竜之)", + "releaseDate": "2019-07-10", + "versionLabel": "TV 輪替 ED/TRF 翻唱;製作署名待核對", + "credits": [ + { + "name": "小林竜之", + "role": "vocals" + } + ] + }, + { + "type": "ED", + "sequence": 5, + "titleJa": "LEGEND OF WIND", + "artistDisplayName": "鷹梁ミナト(CV:五十嵐雅)", + "releaseDate": "2019-07-10", + "versionLabel": "TV 輪替 ED/TRF 翻唱;製作署名待核對", + "credits": [ + { + "name": "五十嵐雅", + "role": "vocals" + } + ] + }, + { + "type": "ED", + "sequence": 6, + "titleJa": "Love & Peace Forever", + "artistDisplayName": "西園寺レオ(CV:永塚拓馬)", + "releaseDate": "2019-07-24", + "versionLabel": "TV 輪替 ED/TRF 翻唱;製作署名待核對", + "credits": [ + { + "name": "永塚拓馬", + "role": "vocals" + } + ] + }, + { + "type": "ED", + "sequence": 7, + "titleJa": "Overnight Sensation ~時代はあなたに委ねてる~", + "artistDisplayName": "涼野ユウ(CV:内田雄馬)", + "releaseDate": "2019-07-24", + "versionLabel": "TV 輪替 ED/TRF 翻唱;製作署名待核對", + "credits": [ + { + "name": "内田雄馬", + "role": "vocals" + } + ] + }, + { + "type": "ED", + "sequence": 8, + "titleJa": "Silver and Gold dance", + "artistDisplayName": "大和アレクサンダー(CV:武内駿輔)", + "releaseDate": "2019-07-24", + "versionLabel": "TV 輪替 ED/TRF 翻唱;製作署名待核對", + "credits": [ + { + "name": "武内駿輔", + "role": "vocals" + } + ] + }, + { + "type": "ED", + "sequence": 9, + "titleJa": "愛がもう少し欲しいよ", + "artistDisplayName": "如月ルヰ(CV:蒼井翔太)", + "releaseDate": "2019-08-07", + "versionLabel": "TV 輪替 ED/TRF 翻唱;製作署名待核對", + "credits": [ + { + "name": "蒼井翔太", + "role": "vocals" + } + ] + }, + { + "type": "ED", + "sequence": 10, + "titleJa": "BRAND NEW TOMORROW", + "artistDisplayName": "一条シン(CV:寺島惇太)", + "releaseDate": "2019-08-07", + "versionLabel": "TV 輪替 ED/TRF 翻唱;製作署名待核對", + "credits": [ + { + "name": "寺島惇太", + "role": "vocals" + } + ] + }, + { + "type": "ED", + "sequence": 11, + "titleJa": "BOY MEETS GIRL", + "artistDisplayName": "一条シン(CV:寺島惇太)、太刀花ユキノジョウ(CV:斉藤壮馬)、香賀美タイガ(CV:畠中祐)、十王院カケル(CV:八代拓)、鷹梁ミナト(CV:五十嵐雅)、西園寺レオ(CV:永塚拓馬)、涼野ユウ(CV:内田雄馬)", + "versionLabel": "TV 輪替 ED/七人合唱的 TRF 翻唱版;發行日期與製作署名待核對", + "credits": [ + { + "name": "寺島惇太", + "role": "vocals" + }, + { + "name": "斉藤壮馬", + "role": "vocals" + }, + { + "name": "畠中祐", + "role": "vocals" + }, + { + "name": "八代拓", + "role": "vocals" + }, + { + "name": "五十嵐雅", + "role": "vocals" + }, + { + "name": "永塚拓馬", + "role": "vocals" + }, + { + "name": "内田雄馬", + "role": "vocals" + } + ] + } + ] +}, { id: "catalog-hangyakusei-million-arthur-2-2019", slug: "hangyakusei-million-arthur-2-2019", titleJa: "叛逆性ミリオンアーサー 第2シーズン", diff --git a/src/data/curated-theme-sources/2019/spring.ts b/src/data/curated-theme-sources/2019/spring.ts index 367148e..7fae87a 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-king-of-prism-sss-2019:OP:1": ["https://kinpri.com/sss/sp/discography/detail.php?id=1016430", "https://www.tv-tokyo.co.jp/broad_tvtokyo/program/detail/201904/25383_201904152535.html"], + "catalog-king-of-prism-sss-2019:ED:1": ["https://kinpri.com/sss/sp/discography/detail.php?id=1016530", "https://www.youtube.com/watch?v=420RJfS0G7Y"], + "catalog-king-of-prism-sss-2019:ED:2": ["https://kinpri.com/sss/sp/discography/detail.php?id=1016531", "https://www.youtube.com/watch?v=420RJfS0G7Y"], + "catalog-king-of-prism-sss-2019:ED:3": ["https://kinpri.com/sss/sp/discography/detail.php?id=1016532", "https://www.youtube.com/watch?v=lmIgYf6WehM"], + "catalog-king-of-prism-sss-2019:ED:4": ["https://kinpri.com/sss/sp/discography/detail.php?id=1016533", "https://www.youtube.com/watch?v=lmIgYf6WehM"], + "catalog-king-of-prism-sss-2019:ED:5": ["https://kinpri.com/sss/sp/discography/detail.php?id=1016534", "https://www.youtube.com/watch?v=lmIgYf6WehM"], + "catalog-king-of-prism-sss-2019:ED:6": ["https://kinpri.com/sss/sp/discography/detail.php?id=1016535", "https://www.youtube.com/watch?v=lp494sLN5Aw"], + "catalog-king-of-prism-sss-2019:ED:7": ["https://kinpri.com/sss/sp/discography/detail.php?id=1016536", "https://www.youtube.com/watch?v=lp494sLN5Aw"], + "catalog-king-of-prism-sss-2019:ED:8": ["https://kinpri.com/sss/sp/discography/detail.php?id=1016537", "https://www.youtube.com/watch?v=lp494sLN5Aw"], + "catalog-king-of-prism-sss-2019:ED:9": ["https://kinpri.com/sss/sp/discography/detail.php?id=1016538", "https://www.youtube.com/watch?v=lCI1f13Vvw4"], + "catalog-king-of-prism-sss-2019:ED:10": ["https://kinpri.com/sss/sp/discography/detail.php?id=1016539", "https://www.youtube.com/watch?v=lCI1f13Vvw4"], + "catalog-king-of-prism-sss-2019:ED:11": ["https://kinpri.com/sss/sp/discography/detail.php?id=1016540", "https://www.youtube.com/watch?v=lCI1f13Vvw4"], "catalog-hangyakusei-million-arthur-2-2019:OP:1": [ "https://www.orsm.jp/discography/open-the-worlds/", "https://www.orsm.jp/%E3%80%8Copen-the-worlds%E3%80%8D%E5%85%88%E8%A1%8C%E9%85%8D%E4%BF%A1%E6%B1%BA%E5%AE%9A/", @@ -281,6 +293,23 @@ const firstPartyUrlsByTheme: Readonly> = { }; const sourceLabelsByUrl: Readonly> = { + "https://kinpri.com/sss/sp/discography/detail.php?id=1016430": "動畫官方商品頁:OP 七人演唱名義與 CD 日期", + "https://kinpri.com/sss/sp/discography/detail.php?id=1016530": "動畫官方商品頁:寒い夜だから・・・ 的演唱者與 CD 日期", + "https://kinpri.com/sss/sp/discography/detail.php?id=1016531": "動畫官方商品頁:masquerade 的演唱者與 CD 日期", + "https://kinpri.com/sss/sp/discography/detail.php?id=1016532": "動畫官方商品頁:Unite! The Night! 的演唱者與 CD 日期", + "https://kinpri.com/sss/sp/discography/detail.php?id=1016533": "動畫官方商品頁:JOY 的演唱者與 CD 日期", + "https://kinpri.com/sss/sp/discography/detail.php?id=1016534": "動畫官方商品頁:LEGEND OF WIND 的演唱者與 CD 日期", + "https://kinpri.com/sss/sp/discography/detail.php?id=1016535": "動畫官方商品頁:Love & Peace Forever 的演唱者與 CD 日期", + "https://kinpri.com/sss/sp/discography/detail.php?id=1016536": "動畫官方商品頁:Overnight Sensation ~時代はあなたに委ねてる~ 的演唱者與 CD 日期", + "https://kinpri.com/sss/sp/discography/detail.php?id=1016537": "動畫官方商品頁:Silver and Gold dance 的演唱者與 CD 日期", + "https://kinpri.com/sss/sp/discography/detail.php?id=1016538": "動畫官方商品頁:愛がもう少し欲しいよ 的演唱者與 CD 日期", + "https://kinpri.com/sss/sp/discography/detail.php?id=1016539": "動畫官方商品頁:BRAND NEW TOMORROW 的演唱者與 CD 日期", + "https://kinpri.com/sss/sp/discography/detail.php?id=1016540": "動畫官方商品頁:BOY MEETS GIRL 七人演唱版(發行日期另待核對)", + "https://www.tv-tokyo.co.jp/broad_tvtokyo/program/detail/201904/25383_201904152535.html": "東京電視台:TV 正篇 OP 用途及七人演唱名義", + "https://www.youtube.com/watch?v=420RJfS0G7Y": "作品官方第 1 批 CD 廣告說明:兩首 TV 輪替 ED 用途", + "https://www.youtube.com/watch?v=lmIgYf6WehM": "作品官方第 2 批 CD 廣告說明:三首 TV 輪替 ED 用途", + "https://www.youtube.com/watch?v=lp494sLN5Aw": "作品官方第 3 批 CD 廣告說明:三首 TV 輪替 ED 用途", + "https://www.youtube.com/watch?v=lCI1f13Vvw4": "作品官方第 4 批 CD 廣告說明:三首 TV 輪替 ED 用途", "https://www.orsm.jp/discography/open-the-worlds/": "ORESAMA 官方:第二季 OP、詞曲編曲署名與 CD 日期", "https://www.orsm.jp/%E3%80%8Copen-the-worlds%E3%80%8D%E5%85%88%E8%A1%8C%E9%85%8D%E4%BF%A1%E6%B1%BA%E5%AE%9A/": "ORESAMA 官方:mora 及其他平台先行配信日期", "https://www.youtube.com/watch?v=0UmEg8PDV3Y": "Lantis:第二季 OP 官方完整 MV、演唱者及製作署名", @@ -418,6 +447,12 @@ const sourceLabelsByUrl: Readonly> = { }; const crossCheckSourcesByAnime: Readonly> = { + "catalog-king-of-prism-sss-2019": { + label: "アニソン・オンライン:2019 春季 TV OP、十一首 ED 與演唱版本交叉核對", + url: "https://anison.online/anime/4114", + language: "ja", + role: "cross_check" + }, "catalog-hangyakusei-million-arthur-2-2019": { label: "アニソン・オンライン:第二季 OP/ED 與演唱組合交叉核對", url: "https://anison.online/anime/1105", diff --git a/tests/e2e/phase1.spec.ts b/tests/e2e/phase1.spec.ts index e4bab09..046351a 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("1952"); + await expect(page.locator("[data-catalog-anime-count]")).toHaveText("1953"); 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("1952"); + await expect(page.locator("[data-catalog-anime-count]")).toHaveText("1953"); 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,952 個作品頁與 4,330 首 OP/ED"); + await expect(page.getByLabel("季度資料狀態")).toContainText("已發布 29 個季度、1,953 個作品頁與 4,342 首 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: /春季動畫.*35 套動畫/ }).click(); + await page.getByRole("link", { name: /春季動畫.*36 套動畫/ }).click(); await expect(page.getByRole("heading", { name: "2019 春季動畫" })).toBeVisible(); - await expect(page.getByText("本季正在補充,目前收錄 35 套 TV 作品(含電視短篇及重編版)。其餘作品、跨季延續及特殊歌曲版本仍待核對。", { exact: true })).toBeVisible(); - await expect(page.locator("[data-anime-card]")).toHaveCount(35); + await expect(page.getByText("本季正在補充,目前收錄 36 套 TV 作品(含電視短篇及重編版)。其餘作品、跨季延續及特殊歌曲版本仍待核對。", { exact: true })).toBeVisible(); + await expect(page.locator("[data-anime-card]")).toHaveCount(36); await expect(page.locator("[data-anime-card] img")).toHaveCount(0); expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); @@ -1681,6 +1681,90 @@ test("anime details keep full-width poster-free headings and readable metadata a } }); +test("KING OF PRISM search and compact song navigation preserve singing identities, cover endings and unverified dates across devices", async ({ page, browser, request }) => { + test.setTimeout(60_000); + const slug = "king-of-prism-sss-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-king-of-prism-sss-2019"); + expect(detail.themes).toHaveLength(12); + expect(detail.themes[11]).not.toHaveProperty("releaseDate"); + 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: "KING OF PRISM -Shiny Seven Stars-", exact: true }).first().click(); + await expect(page.locator(".anime-hero__zh")).toHaveText("星光王子 KING OF PRISM -Shiny Seven Stars-"); + await expect(page.locator(".anime-hero__meta")).toContainText("每週一 25:35"); + await expect(page.locator(".theme-card")).toHaveCount(12); + const disclosure = page.locator(".theme-navigation"); + await expect(disclosure).not.toHaveAttribute("open"); + await expect(disclosure.locator("summary")).toContainText("1 首 OP · 11 首 ED"); + await disclosure.locator("summary").focus(); + await page.keyboard.press("Enter"); + const navigation = page.getByRole("navigation", { name: "本作品歌曲" }); + await expect(navigation.getByRole("link")).toHaveCount(12); + expect((await navigation.boundingBox())!.height).toBeLessThanOrEqual(844 * 0.55 + 1); + await navigation.locator(`a[href="#theme-${slug}-ed-11"]`).focus(); + await page.keyboard.press("Enter"); + const group = page.locator(`#theme-${slug}-ed-11`); + await expect(group).toBeFocused(); + await expect(group).toContainText("發行日期與製作署名待核對"); + await expect(group.locator("time")).toHaveCount(0); + await expect(group.locator(".theme-card__artist")).toContainText("一条シン(CV:寺島惇太)"); + await expect(group.locator(".theme-card__artist")).toContainText("涼野ユウ(CV:内田雄馬)"); + await group.getByRole("link", { name: "搜尋「寺島惇太」的歌曲", exact: true }).focus(); + await page.keyboard.press("Enter"); + await page.getByLabel("年份", { exact: true }).selectOption("2019"); + await page.getByLabel("季度", { exact: true }).selectOption("spring"); + await page.getByLabel("歌曲用途", { exact: true }).selectOption("ED"); + await page.getByRole("searchbox", { name: "搜尋動畫、歌曲或創作者" }).fill("小林竜之"); + await expect(page.locator("[data-catalog-result]")).toHaveCount(1); + await page.getByRole("link", { name: "JOY", exact: true }).click(); + const joy = page.locator(`#theme-${slug}-ed-4`); + await expect(joy).toBeFocused(); + await expect(joy.locator(".theme-card__artist")).toHaveText("高田馬場ジョージGS(CV:小林竜之)"); + await expect(joy.locator("time")).toHaveText("2019-07-10"); + await joy.locator("details summary").focus(); + await page.keyboard.press("Enter"); + await expect(joy.locator("details")).toHaveAttribute("open"); + await expect(joy.locator('a[href="https://www.youtube.com/watch?v=lmIgYf6WehM"]')).toBeVisible(); + await expect(page.getByRole("button", { name: /載入 YouTube 影片/ })).toHaveCount(0); + await expect(page.locator("iframe")).toHaveCount(0); + expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); + } + expect(mediaRequests).toEqual([]); + 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: /春季動畫.*36 套動畫/ }).click(); + await nativePage.getByRole("link", { name: "查看 KING OF PRISM -Shiny Seven Stars-(星光王子 KING OF PRISM -Shiny Seven Stars-)", exact: true }).click(); + await expect(nativePage).toHaveURL(new RegExp(`/anime/${slug}/$`)); + await nativePage.locator(".theme-navigation summary").focus(); + await nativePage.keyboard.press("Enter"); + await nativePage.getByRole("navigation", { name: "本作品歌曲" }).locator(`a[href="#theme-${slug}-ed-11"]`).focus(); + await nativePage.keyboard.press("Enter"); + await expect(nativePage.locator(`#theme-${slug}-ed-11`)).toBeFocused(); + await expect(nativePage.locator(`#theme-${slug}-ed-11 time`)).toHaveCount(0); + await nativePage.locator(`#theme-${slug}-ed-11 details summary`).focus(); + await nativePage.keyboard.press("Enter"); + await expect(nativePage.locator(`#theme-${slug}-ed-11 a[href="https://kinpri.com/sss/sp/discography/detail.php?id=1016540"]`)).toBeVisible(); + await expect(nativePage.locator("iframe")).toHaveCount(0); + expect(await nativePage.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); + } finally { + await context.close(); + } +}); + test("Hangyaku second season preserves original song versions, missing credits and responsive source navigation", async ({ page, browser, request }) => { test.setTimeout(60_000); const slug = "hangyakusei-million-arthur-2-2019"; @@ -1735,7 +1819,7 @@ test("Hangyaku second season preserves original song versions, missing credits a try { const nativePage = await context.newPage(); await nativePage.goto(`${e2eOrigin}/catalog/2019/`, { waitUntil: "domcontentloaded" }); - await nativePage.getByRole("link", { name: /春季動畫.*35 套動畫/ }).click(); + await nativePage.getByRole("link", { name: /春季動畫.*36 套動畫/ }).click(); await nativePage.getByRole("link", { name: /叛逆性ミリオンアーサー 第2シーズン/ }).first().click(); await expect(nativePage.locator(".theme-card")).toHaveCount(2); await expect(nativePage.locator(`#theme-${slug}-ed-1 a[href="https://catalog.bandainamcomusiclive.co.jp/release/68378/"]`)).toBeVisible(); @@ -1804,7 +1888,7 @@ test("CLIMAX SEASON search preserves monthly ensemble endings, individual singer try { const nativePage = await context.newPage(); await nativePage.goto(`${e2eOrigin}/catalog/2019/`, { waitUntil: "domcontentloaded" }); - await nativePage.getByRole("link", { name: /春季動畫.*35 套動畫/ }).click(); + await nativePage.getByRole("link", { name: /春季動畫.*36 套動畫/ }).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(); @@ -1873,7 +1957,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: /春季動畫.*35 套動畫/ }).click(); + await nativePage.getByRole("link", { name: /春季動畫.*36 套動畫/ }).click(); await nativePage.getByRole("link", { name: /妖怪ウォッチ!/ }).first().click(); const sources = nativePage.locator(`#theme-${slug}-ed-1 details`); await sources.locator("summary").focus(); @@ -1930,7 +2014,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: /春季動畫.*35 套動畫/ }).click(); + await nativePage.getByRole("link", { name: /春季動畫.*36 套動畫/ }).click(); await nativePage.getByRole("link", { name: /爆丸バトルプラネット/ }).first().click(); await expect(nativePage).toHaveURL(new RegExp(`/anime/${slug}/$`)); await expect(nativePage.locator(".theme-card")).toHaveCount(3); @@ -2071,7 +2155,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: /春季動畫.*35 套動畫/ }).click(); + await nativePage.getByRole("link", { name: /春季動畫.*36 套動畫/ }).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); @@ -2133,7 +2217,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: /春季動畫.*35 套動畫/ }).click(); + await nativePage.getByRole("link", { name: /春季動畫.*36 套動畫/ }).click(); await nativePage.getByRole("link", { name: /けだまのゴンじろー/ }).first().click(); await expect(nativePage).toHaveURL(/\/anime\/gonjiro-2019\/$/); await expect(nativePage.locator(".theme-card")).toHaveCount(2); @@ -2443,7 +2527,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("35"); + await expect(page.locator("[data-result-count]")).toHaveText("36"); await expect(page.getByRole("checkbox", { name: "有 OP" })).toBeFocused(); expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); } @@ -2466,7 +2550,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(35); + await expect(nativePage.locator("[data-anime-card]")).toHaveCount(36); 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 668612f..119ede6 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: 1952 + animeCount: 1953 }); }); diff --git a/tests/unit/catalog-navigation.test.ts b/tests/unit/catalog-navigation.test.ts index c229856..6554e31 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(1952); + expect(searchCatalog(index, defaults)).toHaveLength(1953); }); 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 9319897..035c5b3 100644 --- a/tests/unit/curated-2019-spring.test.ts +++ b/tests/unit/curated-2019-spring.test.ts @@ -10,6 +10,76 @@ 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 KING OF PRISM's TV premiere separately from the introductory program and theatrical chapters", async () => { + const seed = curated2019SpringSeeds.find(({ id }) => id === "catalog-king-of-prism-sss-2019")!; + expect(seed).toBeDefined(); + expect(seed).toMatchObject({ startDate: "2019-04-15", editorialWeekday: 1, broadcastTimeJst: "25:35", seasonIds: ["2019-spring"] }); + const detail = curatedAnimeDetails.find(({ id }) => id === seed.id)!; + expect(detail).toMatchObject({ slug: "king-of-prism-sss-2019", titleJa: "KING OF PRISM -Shiny Seven Stars-", titleZhHant: "星光王子 KING OF PRISM -Shiny Seven Stars-", 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://www.tv-tokyo.co.jp/broad_tvtokyo/program/detail/201904/25383_201904152535.html", role: "identifier", language: "ja" })); + expect(detail.sources).toContainEqual(expect.objectContaining({ url: "https://www.linetv.tw/drama/18245/eps/7", role: "first_party", language: "zh-Hant" })); + 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("retains KING OF PRISM's eleven TV cover endings and the reviewed singing voices without importing insert songs", () => { + const detail = curatedAnimeDetails.find(({ id }) => id === "catalog-king-of-prism-sss-2019")!; + expect(detail).toBeDefined(); + const themes = detail.themes; + expect(themes.map(({ type, titleJa }) => [type, titleJa])).toEqual([ + ["OP", "Shiny Seven Stars!"], ["ED", "寒い夜だから・・・"], ["ED", "masquerade"], + ["ED", "Unite! The Night!"], ["ED", "JOY"], ["ED", "LEGEND OF WIND"], + ["ED", "Love & Peace Forever"], ["ED", "Overnight Sensation ~時代はあなたに委ねてる~"], + ["ED", "Silver and Gold dance"], ["ED", "愛がもう少し欲しいよ"], ["ED", "BRAND NEW TOMORROW"], ["ED", "BOY MEETS GIRL"] + ]); + expect(themes[4]).toMatchObject({ artistDisplayName: "高田馬場ジョージGS(CV:小林竜之)", credits: [{ name: "小林竜之", role: "vocals" }] }); + expect(themes[0]?.credits.map(({ name }) => name)).toEqual(["寺島惇太", "斉藤壮馬", "畠中祐", "八代拓", "五十嵐雅", "永塚拓馬", "内田雄馬"]); + expect(themes[11]?.credits).toEqual(themes[0]?.credits); + expect(themes.every(({ credits }) => credits.every(({ role }) => role === "vocals"))).toBe(true); + expect(themes.some(({ titleJa }) => /366LOVE|虹色CROWN|survival dAnce|ナナイロノチカイ|プラトニックソード|JOKER KISS/.test(titleJa))).toBe(false); + expect(themes.map(({ releaseDate }) => releaseDate)).toEqual([ + "2019-04-24", "2019-06-26", "2019-06-26", "2019-07-10", "2019-07-10", "2019-07-10", + "2019-07-24", "2019-07-24", "2019-07-24", "2019-08-07", "2019-08-07", undefined + ]); + expect(themes[11]?.versionLabel).toContain("發行日期與製作署名待核對"); + }); + + it("uses KING OF PRISM's publisher CM descriptions only as purpose evidence and withholds conflicting dates and playback media", () => { + const detail = curatedAnimeDetails.find(({ id }) => id === "catalog-king-of-prism-sss-2019")!; + expect(detail).toBeDefined(); + for (const theme of detail.themes) { + 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://anison.online/anime/4114", role: "cross_check", language: "ja" })); + expect(theme.sources.some(({ url, role }) => url.startsWith("https://kinpri.com/sss/sp/discography/detail.php?id=") && role === "first_party")).toBe(true); + expect(theme.videos).toEqual([]); + } + expect(detail.themes[1]?.sources).toContainEqual(expect.objectContaining({ url: "https://www.youtube.com/watch?v=420RJfS0G7Y", role: "first_party" })); + expect(detail.themes[11]?.sources).toContainEqual(expect.objectContaining({ url: "https://www.youtube.com/watch?v=lCI1f13Vvw4", role: "first_party" })); + expect(JSON.parse(JSON.stringify(detail.themes[11]))).not.toHaveProperty("releaseDate"); + }); + + it("finds KING OF PRISM's individual singers and ensemble songs without treating the speaking actor or original TRF as the cover singer", async () => { + const index = buildCatalogSearchIndex((await loadCatalogSearchData(new CuratedProvider(), true)).entries); + const filters = { year: "2019", quarter: "spring", type: "all" } as const; + const slug = "king-of-prism-sss-2019"; + const title = searchCatalog(index, { ...filters, query: "星光王子", scope: "anime" }); + expect(title.map(({ anime: item }) => item.slug)).toEqual([slug]); + for (const [query, type, expected] of [ + ["小林竜之", "ED", ["JOY"]], ["杉田智和", "ED", []], ["TRF", "ED", []], + ["寺島惇太", "OP", ["Shiny Seven Stars!"]], ["寺島惇太", "ED", ["BRAND NEW TOMORROW", "BOY MEETS GIRL"]] + ] as const) { + const result = searchCatalog(index, { ...filters, query, type, scope: "creators" }).find(({ anime: item }) => item.slug === slug); + expect(result?.themes.map(({ titleJa }) => titleJa) ?? []).toEqual(expected); + } + }); + it("identifies Hangyaku's second TV season without importing the first season or unaired special", async () => { const seed = curated2019SpringSeeds.find(({ id }) => id === "catalog-hangyakusei-million-arthur-2-2019")!; expect(seed).toMatchObject({ startDate: "2019-04-04", editorialWeekday: 4, broadcastTimeJst: "22:00", seasonIds: ["2019-spring"] }); @@ -883,9 +953,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(35); + expect(spring.anime).toHaveLength(36); expect(spring.coverageNote).toContain("跨季延續及特殊歌曲版本仍待核對"); - expect(curated2019SpringSeeds.flatMap(({ themes }) => themes)).toHaveLength(101); + expect(curated2019SpringSeeds.flatMap(({ themes }) => themes)).toHaveLength(113); 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 0b796a3..e73755f 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, 35]); + 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, 36]); 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-08"] ]); - expect(curatedAnimeDetails).toHaveLength(1952); - expect(curatedAnimeDetails.filter((anime) => anime.themes.length > 0)).toHaveLength(1588); - expect(curatedAnimeDetails.flatMap((anime) => anime.themes)).toHaveLength(4330); - expect(curatedAnimeDetails.filter((anime) => anime.themeAvailability === "documented")).toHaveLength(1588); + expect(curatedAnimeDetails).toHaveLength(1953); + expect(curatedAnimeDetails.filter((anime) => anime.themes.length > 0)).toHaveLength(1589); + expect(curatedAnimeDetails.flatMap((anime) => anime.themes)).toHaveLength(4342); + expect(curatedAnimeDetails.filter((anime) => anime.themeAvailability === "documented")).toHaveLength(1589); 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,7 @@ describe("curated public catalogue", () => { } for (const item of anime.themes) { - const expectedVerifiedAt = ["catalog-cinderella-girls-climax-2019", "catalog-hangyakusei-million-arthur-2-2019"].includes(anime.id) + const expectedVerifiedAt = ["catalog-cinderella-girls-climax-2019", "catalog-hangyakusei-million-arthur-2-2019", "catalog-king-of-prism-sss-2019"].includes(anime.id) ? "2026-09-08" : anime.id === "curated-111048" || curatedSeasonDetails.find(({ id }) => id === "2019-summer")?.anime.some(({ id }) => id === anime.id) diff --git a/tests/unit/curated-season-registry.test.ts b/tests/unit/curated-season-registry.test.ts index f9955a7..42d0526 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(1952); + expect(ownedSeeds).toHaveLength(1953); 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 2170dc3..b384849 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, - 101 + 113 ]); const keys = curatedThemeSourceRegistry.flatMap(({ overrides }) => Object.keys(overrides)); - expect(keys).toHaveLength(2068); + expect(keys).toHaveLength(2080); expect(new Set(keys).size).toBe(keys.length); expect(Object.keys(curatedThemeSourceOverrides)).toHaveLength(keys.length); }); diff --git a/tests/unit/page-data.test.ts b/tests/unit/page-data.test.ts index 844735c..ca6549b 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(1952); - expect(new Set(result.entries.map(({ anime }) => anime.slug)).size).toBe(1952); - expect(result.entries.filter(({ anime }) => anime.themes.length > 0)).toHaveLength(1588); + expect(result.entries).toHaveLength(1953); + expect(new Set(result.entries.map(({ anime }) => anime.slug)).size).toBe(1953); + expect(result.entries.filter(({ anime }) => anime.themes.length > 0)).toHaveLength(1589); }); it("returns a safe public error state without exposing provider details", async () => {