diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d83792..2e2c809 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.54.0 — 2019 春季《ULTRAMAN》作品識別 + +- 收錄 Netflix 第一季,保留 2019 年 4 月 1 日日本上線、全 13 話及繁中/日文/英文第一方識別來源。 +- 歌曲用途仍待核對,維持待補充空狀態,不混入 2020 電視版、續作、真人影集或電影的歌曲、署名與媒體。 +- 春季共 38 套作品(37 套 TV、1 套網絡連載)與 115 筆 OP/ED;本季與夏季仍在補充,2025 秋季尚未收錄。 + ## v1.53.0 — 2019 春季《偶像學園Friends! 第二季》 - 收錄 TV 第二季與 OP〈ひとりじゃない!〉、一般 ED〈Be star〉,保留四人角色合唱、ひびき獨唱名義及共同詞曲編曲。 diff --git a/README.md b/README.md index 01dd74a..1797b58 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,954 unique titles and 4,344 known OP/ED records. The 2019 spring snapshot contains thirty-seven 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,955 unique titles and 4,344 known OP/ED records. The 2019 spring snapshot contains thirty-seven TV titles, including television shorts and a TV recut, plus one web series; 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.53.0**。新增 2019 春季《偶像學園Friends! 第二季》的四人合唱 OP 與一般 ED,保留共同詞曲編曲,分清雙人改編、舊季度歌曲與未核對特殊片尾。目錄共有 29 個季度、1,954 套作品與 4,344 筆 OP/ED;春季為 37 套作品、115 筆歌曲,春、夏季仍在補充,2025 秋季尚未收錄。 +目前 source 版本:**v1.54.0**。新增 2019 春季《ULTRAMAN》Netflix 第一季的作品識別與上線資料;歌曲用途仍待核對,不混入 2020 電視版或後續季度歌曲。目錄共有 29 個季度、1,955 套作品與 4,344 筆 OP/ED;春季為 38 套作品、115 筆歌曲,春、夏季仍在補充,2025 秋季尚未收錄。 導覽延續固定五個入口與年代、年份、季度目錄;搜尋可按動畫、歌曲、創作者、年份、季度及 OP/ED 篩選,每頁顯示 12 套作品。搜尋完全在瀏覽器內進行,YouTube 只於明確操作後連線。網站與同源靜態 JSON API 共用已審閱資料,無 application backend 或 database binding。依賴安全 gate 繼續鎖定 `fast-uri` 3.1.6 並稽核開發依賴。 diff --git a/docs/DATA_PROVENANCE.md b/docs/DATA_PROVENANCE.md index 96aa5ad..75b6b79 100644 --- a/docs/DATA_PROVENANCE.md +++ b/docs/DATA_PROVENANCE.md @@ -31,9 +31,9 @@ API、資料庫、商標或媒體重用權。完整授權邊界見 `first_party` 表示動畫製作/發行方、唱片公司、藝人官方渠道、官方影片或正式播出機構的公開頁面。`cross_check` 表示用於發現缺口及交叉比對的公開索引。兩種角色都是來源分類,不代表數值信心或內容評分。 -2019 春季目前整理 37 套 TV 作品(含電視短篇及重編版),夏季整理 38 套 TV 作品及 4 套網絡連載;兩季的其他作品、跨季延續與特殊歌曲仍待核對;`coverageNote` 會在季度頁及 API 公開說明本季範圍。 +2019 春季目前整理 37 套 TV 作品(含電視短篇及重編版)及 1 套網絡連載,夏季整理 38 套 TV 作品及 4 套網絡連載;兩季的其他作品、跨季延續與特殊歌曲仍待核對;`coverageNote` 會在季度頁及 API 公開說明本季範圍。 -目前二十九季快照的 season references、1,954 個作品及 4,344 筆 OP/ED 都有結構化 provenance;4,344 筆歌曲全部至少保留一個 `first_party` 與一個 `cross_check` 來源。`tests/unit/curated-catalog.test.ts` 鎖定來源數量、角色、語言、HTTPS、核對日期、Mock URL 禁止條件,以及 legacy labels 與結構化 ledger 的一致性。 +目前二十九季快照的 season references、1,955 個作品及 4,344 筆 OP/ED 都有結構化 provenance;4,344 筆歌曲全部至少保留一個 `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 af386bd..2847223 100644 --- a/docs/DATA_SOURCES.md +++ b/docs/DATA_SOURCES.md @@ -11,7 +11,7 @@ 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 春季 37 套 TV 作品(含電視短篇及重編版),共 1,954 個作品詳情、4,344 筆 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 春季 38 套作品(37 套 TV,含電視短篇及重編版;1 套網絡連載),共 1,955 個作品詳情、4,344 筆 OP/ED 與 567 個歌曲層 YouTube 直連;其中 1,812 筆已保留官方或正式授權影片 metadata。 每季先以繁中年度動畫表的對應月份建立日曆清單,再與 Annict、Bangumi 及 AniList identifier 取聯集核對。OP/ED 以作品官網和官方 YouTube 為主,並用 AnimeThemes、UZUREA 與 UtaTen 等季度主題曲表交叉確認;季度歌曲索引未收錄的作品,須返回 Netflix、唱片公司、IP 官網、正式播出機構或製作公司等第一方頁面補證。台港正式代理、串流或出版社名稱優先;未有可確認的繁中名稱時保留原文,不宣稱為正式授權譯名。 diff --git a/docs/PROJECT_PLAN.md b/docs/PROJECT_PLAN.md index 5db3b09..2e7bec9 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 春季 37 套非成人 TV/網絡連載作品; -- 跨季作品共用詳情頁,共 1,954 個唯一作品及 4,344 筆已審閱 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 春季 38 套非成人 TV/網絡連載作品; +- 跨季作品共用詳情頁,共 1,955 個唯一作品及 4,344 筆已審閱 OP/ED; - 五個固定導覽入口、年代/年份/季度目錄、星期一至星期日及不定期分組、分頁本機搜尋、動畫詳情與同源 static JSON API; - 每個季度、作品及歌曲公開來源、語言、review state 及核對日期;每首歌曲同時具備第一方/交叉核對來源; - 官方/licensed YouTube 導航、明確第三方連線同意、來源歸屬及無圖 fallback; @@ -88,6 +88,7 @@ - **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。 - **v1.53.0 source**:2019 春季增加《偶像學園Friends! 第二季》、四人合唱 OP 與一般 ED,保留共同作詞、作曲及編曲者,使用逐曲交叉來源;未核對特殊片尾、聲優歌唱署名、媒體與衝突日期留空。春季共 37 套作品與 115 筆歌曲;發布證據見對應 GitHub Release。 +- **v1.54.0 source**:2019 春季增加《ULTRAMAN》Netflix 第一季,核對日本上線日期與 13 話範圍;用途尚未核實的歌曲保留空狀態,不套用 2020 電視版或續作歌曲。春季共 37 套 TV 作品及 1 套網絡連載、115 筆歌曲;發布證據見對應 GitHub Release。 - 正式網站使用 custom domain;非正式 Cloudflare hostname 不在公開文檔記錄; - 私有 backend、資料庫、crawler、帳戶設定、憑證與內部規則不屬於本公開 repository。 diff --git a/docs/QA_V1_54_2019_SPRING_ULTRAMAN.md b/docs/QA_V1_54_2019_SPRING_ULTRAMAN.md new file mode 100644 index 0000000..814ce02 --- /dev/null +++ b/docs/QA_V1_54_2019_SPRING_ULTRAMAN.md @@ -0,0 +1,26 @@ +# v1.54.0 QA:2019 春季《ULTRAMAN》作品識別 + +來源核對日期:2026-09-08。本切片收錄 Netflix 第一季的作品識別,歌曲維持待補充狀態。春季共 38 套作品(37 套 TV、1 套網絡連載)與 115 筆歌曲;春、夏季仍在補充,2025 秋季尚未收錄。 + +## 資料與來源 + +- [Netflix 繁中公告](https://about.netflix.com/zh_tw/news/anime-production-line-deal)、[動畫官方第一季介紹](https://anime.heros-ultraman.com/story/)與 [YourAnimes 條目](https://youranimes.tw/animes/3065)核對《ULTRAMAN》及 2019-04-01 上線日期;保留原文名稱,不另外推定繁中譯名或 Romaji。 +- [円谷製作公告](https://tsuburaya-prod.com/news/4883)的第一季資料確認 13 話範圍。播出欄明示 Netflix 第一季網絡配信,沒有把上線星期推定為每週更新,也不合併 2020 電視版、後續季度、真人影集或電影。 +- [日文官方音樂頁](https://anime.heros-ultraman.com/music/)與[唱片公司公告](https://lantis.jp/artist/oldcodex/news_1558177200.html)稱〈Sight Over The Battle〉為主題歌;[英文官方公告](https://tsuburaya-prod.com/news/4883)使用 Opening Song 標示。尚未核實原配信版的確切用途,歌曲保持空白,不由一般主題歌或單一索引推定 OP/ED。 +- [Void_Chords 官方公告](https://void-chords.lantis.jp/news/151/)明示〈my ID〉屬 2020 年電視播出版;這個切片不把翌年的電視版或續作歌曲套入 2019 原配信版。 +- 空狀態表示歌曲資料尚待核對,不表示作品沒有歌曲。API 保留既有 `not_announced` 相容值,介面顯示「主題曲資料尚待補充」;不新增歌曲、署名、圖片、第三方 ID 或影片。 +- 只保留必要事實、短名稱及來源連結;不複製介紹、整頁資料、歌詞或媒體。 + +## 本機驗收 + +- `npm run catalog:check`:255 項通過;unit suite:358 項通過。新增測試驗證作品識別、API 往返、來源語言與日期,以及無歌曲作品的搜尋/用途篩選。 +- `npm run check` 的公開邊界、依賴稽核、lint、unit、build 與 Cloudflare dry-run 通過。首次瀏覽器測試有一項新測試只比對來源標題,未包含連結的完整可存取名稱;修正定位並核對 URL 後,重新執行 `npm run test:e2e`,53 項全部通過。 +- 完整與正式依賴稽核均為 0 項漏洞;`fast-uri` 維持 3.1.6,既有 8 項安全回歸包含在 unit suite。 +- 建置 1,999 份 HTML,3,997 份產物通過公開邊界;Cloudflare dry-run 沒有 runtime binding。 +- 1,983 份既有 API 完全相同,只更新春季清單並新增一份作品詳情 API;沒有移除。既有歌曲與影片輸出保持相同。 +- Browser plugin 未提供,使用既有 Playwright。1280、960、390、320px 及 390px 無 JavaScript 共 15 個畫面通過;沒有水平溢出、framework overlay、page error、console error、預先 YouTube 請求或 iframe。已目視桌面詳情與手機來源換行。 +- 操作涵蓋作品搜尋、年份/季度篩選、OP/ED 篩選排除未知歌曲、空狀態與無 JavaScript 的年份 → 季度 → 作品。完整回歸亦涵蓋鍵盤、返回、分頁、離線、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 5835e72..f7997f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "anisonary", - "version": "1.53.0", + "version": "1.54.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "anisonary", - "version": "1.53.0", + "version": "1.54.0", "license": "SEE LICENSE IN LICENSING.md", "dependencies": { "@astrojs/sitemap": "^3.7.3", diff --git a/package.json b/package.json index 451c14a..bd0b93f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "anisonary", - "version": "1.53.0", + "version": "1.54.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 f1b6c13..ebf3bf7 100644 --- a/src/data/curated-data.ts +++ b/src/data/curated-data.ts @@ -4610,6 +4610,17 @@ const animeOfficialSourceOverrides: Record = { + "catalog-ultraman-2019": [{ + label: "Netflix 繁中公告:作品名稱與 2019 年 4 月上線日期", + url: "https://about.netflix.com/zh_tw/news/anime-production-line-deal", + language: "zh-Hant", + role: "first_party" + }, { + label: "円谷製作:Netflix 第 1 季、13 話範圍與後續季度識別;歌曲用途另待核對", + url: "https://tsuburaya-prod.com/news/4883", + language: "en", + role: "first_party" + }], "catalog-aikatsu-friends-2-2019": [{ label: "BANDAI:2019 年 TV 第二季的正式日文名稱與遊戲版本分界", url: "https://bandai-a.akamaihd.net/corp/press/100000741765491.pdf", diff --git a/src/data/curated-season-registry.ts b/src/data/curated-season-registry.ts index 579bff9..41495a4 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: "本季正在補充,目前收錄 37 套 TV 作品(含電視短篇及重編版)。其餘作品、跨季延續及特殊歌曲版本仍待核對。", + coverageNote: "本季正在補充,目前收錄 37 套 TV 作品及 1 套網絡連載(含電視短篇及重編版)。其餘作品、跨季延續及特殊歌曲版本仍待核對。", seeds: curated2019SpringSeeds, animeIds: curated2019SpringAnimeIds } diff --git a/src/data/curated-seeds/2019/spring.ts b/src/data/curated-seeds/2019/spring.ts index fd8bfb7..269a297 100644 --- a/src/data/curated-seeds/2019/spring.ts +++ b/src/data/curated-seeds/2019/spring.ts @@ -1698,6 +1698,26 @@ const strikeWitches501EndingRows = [ ] as const; const independentSpringSeeds: NativeCuratedAnimeSeed[] = [{ + id: "catalog-ultraman-2019", + slug: "ultraman-2019", + titleJa: "ULTRAMAN", + titleZhHant: "ULTRAMAN", + seasonIds: ["2019-spring"], + startDate: "2019-04-01", + editorialWeekday: 1, + broadcastLabel: "Netflix 日本上線 2019-04-01;網絡配信第 1 季,全 13 話", + status: "finished", + verifiedAt: "2026-09-08", + officialSiteUrl: "https://anime.heros-ultraman.com/", + identifierSource: { + label: "動畫官方:Netflix 第 1 季識別與 2019 年上線日期", + url: "https://anime.heros-ultraman.com/story/", + language: "ja" + }, + wikipediaUrl: "https://youranimes.tw/animes/3065", + sourceReferenceUrls: ["https://youranimes.tw/bangumi/201904"], + themes: [] +}, { id: "catalog-aikatsu-friends-2-2019", slug: "aikatsu-friends-2-2019", titleJa: "アイカツフレンズ!~かがやきのジュエル~", diff --git a/tests/e2e/phase1.spec.ts b/tests/e2e/phase1.spec.ts index 5867f54..faaa8c4 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("1954"); + await expect(page.locator("[data-catalog-anime-count]")).toHaveText("1955"); 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("1954"); + await expect(page.locator("[data-catalog-anime-count]")).toHaveText("1955"); 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,954 個作品頁與 4,344 首 OP/ED"); + await expect(page.getByLabel("季度資料狀態")).toContainText("已發布 29 個季度、1,955 個作品頁與 4,344 首 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: /春季動畫.*37 套動畫/ }).click(); + await page.getByRole("link", { name: /春季動畫.*38 套動畫/ }).click(); await expect(page.getByRole("heading", { name: "2019 春季動畫" })).toBeVisible(); - await expect(page.getByText("本季正在補充,目前收錄 37 套 TV 作品(含電視短篇及重編版)。其餘作品、跨季延續及特殊歌曲版本仍待核對。", { exact: true })).toBeVisible(); - await expect(page.locator("[data-anime-card]")).toHaveCount(37); + await expect(page.getByText("本季正在補充,目前收錄 37 套 TV 作品及 1 套網絡連載(含電視短篇及重編版)。其餘作品、跨季延續及特殊歌曲版本仍待核對。", { exact: true })).toBeVisible(); + await expect(page.locator("[data-anime-card]")).toHaveCount(38); await expect(page.locator("[data-anime-card] img")).toHaveCount(0); expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); @@ -1681,6 +1681,56 @@ test("anime details keep full-width poster-free headings and readable metadata a } }); +test("ULTRAMAN original-season browsing preserves its web identity and missing-song state across devices", async ({ page, browser, request }) => { + test.setTimeout(60_000); + const slug = "ultraman-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).toMatchObject({ id: "catalog-ultraman-2019", themeAvailability: "not_announced", themes: [] }); + 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("ULTRAMAN"); + await page.getByLabel("年份", { exact: true }).selectOption("2019"); + await page.getByLabel("季度", { exact: true }).selectOption("spring"); + await expect(page.locator("[data-catalog-result]")).toHaveCount(1); + for (const type of ["OP", "ED"]) { + await page.getByLabel("歌曲用途").selectOption(type); + await expect(page.locator("[data-catalog-result]")).toHaveCount(0); + } + await page.getByLabel("歌曲用途").selectOption("all"); + await page.getByRole("link", { name: "ULTRAMAN", exact: true }).first().click(); + await expect(page).toHaveURL(new RegExp(`/anime/${slug}/$`)); + await expect(page.locator(".anime-hero__meta")).toContainText("網絡配信第 1 季,全 13 話"); + await expect(page.getByRole("heading", { name: "主題曲資料尚待補充" })).toBeVisible(); + await expect(page.getByRole("heading", { name: "本作不設獨立 OP/ED" })).toHaveCount(0); + await expect(page.locator(".theme-card, iframe, .anime-hero img, .anime-hero__romaji")).toHaveCount(0); + const netflixSource = page.getByRole("link", { name: /^Netflix 繁中公告:作品名稱與 2019 年 4 月上線日期 / }); + await expect(netflixSource).toBeVisible(); + await expect(netflixSource).toHaveAttribute("href", "https://about.netflix.com/zh_tw/news/anime-production-line-deal"); + expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); + } + const native = await browser.newContext({ javaScriptEnabled: false, viewport: { width: 390, height: 844 } }); + try { + const nativePage = await native.newPage(); + await nativePage.goto(`${e2eOrigin}/catalog/2019/`); + await nativePage.getByRole("link", { name: /春季動畫.*38 套動畫/ }).click(); + await nativePage.getByRole("link", { name: "查看 ULTRAMAN", exact: true }).click(); + await expect(nativePage).toHaveURL(new RegExp(`/anime/${slug}/$`)); + await expect(nativePage.getByRole("heading", { name: "主題曲資料尚待補充" })).toBeVisible(); + await expect(nativePage.locator(".theme-card, iframe")).toHaveCount(0); + expect(await nativePage.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); + } finally { + await native.close(); + } + expect(mediaRequests).toEqual([]); +}); + test("Aikatsu Friends second-season search preserves the four-character opening, joint writers and withheld release dates across devices", async ({ page, browser, request }) => { test.setTimeout(60_000); const slug = "aikatsu-friends-2-2019"; @@ -1731,7 +1781,7 @@ test("Aikatsu Friends second-season search preserves the four-character opening, try { const nativePage = await context.newPage(); await nativePage.goto(`${e2eOrigin}/catalog/2019/`, { waitUntil: "domcontentloaded" }); - await nativePage.getByRole("link", { name: /春季動畫.*37 套動畫/ }).click(); + await nativePage.getByRole("link", { name: /春季動畫.*38 套動畫/ }).click(); await nativePage.getByRole("link", { name: "查看 アイカツフレンズ!~かがやきのジュエル~(偶像學園Friends! 第二季)", exact: true }).click(); await expect(nativePage).toHaveURL(new RegExp(`/anime/${slug}/$`)); await expect(nativePage.locator(".theme-card")).toHaveCount(2); @@ -1811,7 +1861,7 @@ test("KING OF PRISM search and compact song navigation preserve singing identiti try { const nativePage = await context.newPage(); await nativePage.goto(`${e2eOrigin}/catalog/2019/`, { waitUntil: "domcontentloaded" }); - await nativePage.getByRole("link", { name: /春季動畫.*37 套動畫/ }).click(); + await nativePage.getByRole("link", { name: /春季動畫.*38 套動畫/ }).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(); @@ -1884,7 +1934,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: /春季動畫.*37 套動畫/ }).click(); + await nativePage.getByRole("link", { name: /春季動畫.*38 套動畫/ }).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(); @@ -1953,7 +2003,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: /春季動畫.*37 套動畫/ }).click(); + await nativePage.getByRole("link", { name: /春季動畫.*38 套動畫/ }).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(); @@ -2022,7 +2072,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: /春季動畫.*37 套動畫/ }).click(); + await nativePage.getByRole("link", { name: /春季動畫.*38 套動畫/ }).click(); await nativePage.getByRole("link", { name: /妖怪ウォッチ!/ }).first().click(); const sources = nativePage.locator(`#theme-${slug}-ed-1 details`); await sources.locator("summary").focus(); @@ -2079,7 +2129,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: /春季動畫.*37 套動畫/ }).click(); + await nativePage.getByRole("link", { name: /春季動畫.*38 套動畫/ }).click(); await nativePage.getByRole("link", { name: /爆丸バトルプラネット/ }).first().click(); await expect(nativePage).toHaveURL(new RegExp(`/anime/${slug}/$`)); await expect(nativePage.locator(".theme-card")).toHaveCount(3); @@ -2220,7 +2270,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: /春季動畫.*37 套動畫/ }).click(); + await nativePage.getByRole("link", { name: /春季動畫.*38 套動畫/ }).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); @@ -2282,7 +2332,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: /春季動畫.*37 套動畫/ }).click(); + await nativePage.getByRole("link", { name: /春季動畫.*38 套動畫/ }).click(); await nativePage.getByRole("link", { name: /けだまのゴンじろー/ }).first().click(); await expect(nativePage).toHaveURL(/\/anime\/gonjiro-2019\/$/); await expect(nativePage.locator(".theme-card")).toHaveCount(2); @@ -2592,7 +2642,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("37"); + await expect(page.locator("[data-result-count]")).toHaveText("38"); await expect(page.getByRole("checkbox", { name: "有 OP" })).toBeFocused(); expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); } @@ -2615,7 +2665,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(37); + await expect(nativePage.locator("[data-anime-card]")).toHaveCount(38); 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 09f1249..474c168 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: 1954 + animeCount: 1955 }); }); diff --git a/tests/unit/catalog-navigation.test.ts b/tests/unit/catalog-navigation.test.ts index cdd411d..8ef58e2 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(1954); + expect(searchCatalog(index, defaults)).toHaveLength(1955); }); 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 b8b202f..875f0f1 100644 --- a/tests/unit/curated-2019-spring.test.ts +++ b/tests/unit/curated-2019-spring.test.ts @@ -9,7 +9,51 @@ import { buildCatalogSearchIndex, searchCatalog } from "@/utils/catalog-search-i 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", () => { +describe("2019 spring reviewed TV and web catalogue", () => { + it("identifies ULTRAMAN's original Netflix season separately from TV broadcast, sequels and live action", async () => { + const seed = curated2019SpringSeeds.find(({ id }) => id === "catalog-ultraman-2019")!; + expect(seed).toBeDefined(); + expect(seed).toMatchObject({ startDate: "2019-04-01", editorialWeekday: 1, seasonIds: ["2019-spring"], status: "finished" }); + expect(seed.broadcastLabel).toContain("網絡配信第 1 季,全 13 話"); + const detail = curatedAnimeDetails.find(({ id }) => id === seed.id)!; + expect(detail).toMatchObject({ slug: "ultraman-2019", titleJa: "ULTRAMAN", titleZhHant: "ULTRAMAN" }); + for (const omitted of ["anilistId", "anilistUrl", "titleRomaji", "posterUrl", "bannerUrl", "broadcastTimeJst"]) { + expect(seed).not.toHaveProperty(omitted); + expect(detail).not.toHaveProperty(omitted); + } + 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 ULTRAMAN's reviewed multilingual identity sources while withholding disputed theme purposes and later TV songs", () => { + const detail = curatedAnimeDetails.find(({ id }) => id === "catalog-ultraman-2019")!; + expect(detail).toBeDefined(); + expect(detail.themes).toEqual([]); + expect(detail.themeAvailability).toBe("not_announced"); + expect(detail.sources).toEqual(expect.arrayContaining([ + expect.objectContaining({ url: "https://anime.heros-ultraman.com/story/", role: "identifier", language: "ja" }), + expect.objectContaining({ url: "https://about.netflix.com/zh_tw/news/anime-production-line-deal", role: "first_party", language: "zh-Hant" }), + expect.objectContaining({ url: "https://tsuburaya-prod.com/news/4883", role: "first_party", language: "en" }), + expect.objectContaining({ url: "https://youranimes.tw/animes/3065", role: "localized_cross_check", language: "zh-Hant" }) + ])); + expect(detail.sources.every(({ verifiedAt }) => verifiedAt === "2026-09-08")).toBe(true); + expect(spring.coverageNote).toContain("37 套 TV 作品及 1 套網絡連載"); + }); + + it("finds ULTRAMAN's original season without duplicating sequels or inventing OP and ED matches", async () => { + const data = await loadCatalogSearchData(new CuratedProvider(), true); + const index = buildCatalogSearchIndex(data.entries); + const results = searchCatalog(index, { query: "ULTRAMAN", scope: "anime", year: "2019", quarter: "spring", type: "all" }); + expect(results.map(({ anime: item }) => item.slug)).toEqual(["ultraman-2019"]); + expect(results[0]?.themes).toEqual([]); + for (const type of ["OP", "ED"] as const) { + expect(searchCatalog(index, { query: "ULTRAMAN", scope: "anime", year: "2019", quarter: "spring", type })).toEqual([]); + } + expect(curatedAnimeDetails.filter(({ slug }) => ["ultraman-season-2", "ultraman-final"].includes(slug))).toHaveLength(2); + }); + it("identifies Aikatsu Friends' second TV season independently from the first season, game update and On Parade", async () => { const seed = curated2019SpringSeeds.find(({ id }) => id === "catalog-aikatsu-friends-2-2019")!; expect(seed).toBeDefined(); @@ -1015,7 +1059,7 @@ describe("2019 spring reviewed TV catalogue", () => { }); it("publishes a partial quarter with reviewed identity and song evidence, without unverified artwork", () => { - expect(spring.anime).toHaveLength(37); + expect(spring.anime).toHaveLength(38); expect(spring.coverageNote).toContain("跨季延續及特殊歌曲版本仍待核對"); expect(curated2019SpringSeeds.flatMap(({ themes }) => themes)).toHaveLength(115); expect(curatedSeasonDetails.some(({ id }) => id === "2025-fall")).toBe(false); diff --git a/tests/unit/curated-catalog.test.ts b/tests/unit/curated-catalog.test.ts index 503abb7..92044e2 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, 37]); + 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, 38]); expect(curatedSeasonDetails.map((season) => [season.id, season.reviewState, season.verifiedAt])).toEqual([ ["2026-summer", "reviewed", "2026-08-02"], ["2026-spring", "reviewed", "2026-08-02"], @@ -70,12 +70,12 @@ describe("curated public catalogue", () => { ["2019-summer", "reviewed", "2026-09-07"], ["2019-spring", "reviewed", "2026-09-08"] ]); - expect(curatedAnimeDetails).toHaveLength(1954); + expect(curatedAnimeDetails).toHaveLength(1955); expect(curatedAnimeDetails.filter((anime) => anime.themes.length > 0)).toHaveLength(1590); expect(curatedAnimeDetails.flatMap((anime) => anime.themes)).toHaveLength(4344); expect(curatedAnimeDetails.filter((anime) => anime.themeAvailability === "documented")).toHaveLength(1590); expect(curatedAnimeDetails.filter((anime) => anime.themeAvailability === "not_used")).toHaveLength(2); - expect(curatedAnimeDetails.filter((anime) => anime.themeAvailability === "not_announced")).toHaveLength(362); + expect(curatedAnimeDetails.filter((anime) => anime.themeAvailability === "not_announced")).toHaveLength(363); const youtubeLinks = curatedAnimeDetails .flatMap((anime) => anime.themes) .flatMap((theme) => theme.links) diff --git a/tests/unit/curated-season-registry.test.ts b/tests/unit/curated-season-registry.test.ts index cd49afa..ee47ce8 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(1954); + expect(ownedSeeds).toHaveLength(1955); expect(new Set(ownedSeeds.map(getCuratedAnimeKey)).size).toBe(ownedSeeds.length); for (const entry of curatedSeasonRegistry) { diff --git a/tests/unit/page-data.test.ts b/tests/unit/page-data.test.ts index 8cc1ebd..b4aba7f 100644 --- a/tests/unit/page-data.test.ts +++ b/tests/unit/page-data.test.ts @@ -65,8 +65,8 @@ 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(1954); - expect(new Set(result.entries.map(({ anime }) => anime.slug)).size).toBe(1954); + expect(result.entries).toHaveLength(1955); + expect(new Set(result.entries.map(({ anime }) => anime.slug)).size).toBe(1955); expect(result.entries.filter(({ anime }) => anime.themes.length > 0)).toHaveLength(1590); });