diff --git a/README.md b/README.md index 91c8fc4..751249d 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Anisonary 是以 Astro + strict TypeScript 建立的動畫歌曲目錄,按季 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,944 unique titles and 4,292 known OP/ED records. The 2019 spring snapshot contains twenty-seven TV titles, including television shorts; summer contains 38 TV titles and four web series. Both quarters are still being expanded, and 2025 fall is not yet included. Fictional Mock Data remains test-only. -目前 source 版本:**v1.40.0**。2019 春季加入《YU-NO 在這世界盡頭詠唱愛的少女》TV 版的 4 筆 OP/ED 與 2 筆官方短版 MV metadata;保留前後篇的歌手交換、完整製作署名及 CD、高解析音源與 TV Size 日期。目錄共有 29 個季度、1,944 套作品與 4,292 筆 OP/ED;春季為 27 套作品、63 筆歌曲,春、夏季仍在補充,2025 秋季尚未收錄。 +目前 source 版本:**v1.40.1**。補上《Fairy gone》春季 OP/ED 的個別演唱者、作曲、編曲及已核對作詞署名,支援按創作者找到對應歌曲,並保留原有團體名義與發行版本。目錄共有 29 個季度、1,944 套作品與 4,292 筆 OP/ED;春季為 27 套作品、63 筆歌曲,春、夏季仍在補充,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.40.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.40.1`;正式發布狀態以對應 Release 驗收摘要為準 | [入口網站 / Live](https://anisonary.k-y.cc) · [版本與驗收 / Releases](https://github.com/kyeunga25/anisonary/releases) · [資料來源 / Sources](docs/DATA_SOURCES.md) · [安全政策 / Security](SECURITY.md) · [授權 / Licence](LICENSING.md) · [版權 / Copyright](COPYRIGHT.md) | ## 技術棧|Technology stack diff --git a/docs/API_HANDOFF.md b/docs/API_HANDOFF.md index 82b56bb..56357a5 100644 --- a/docs/API_HANDOFF.md +++ b/docs/API_HANDOFF.md @@ -1,6 +1,6 @@ # Anisonary Public Static API v1 Contract -本文件定義同源靜態 JSON API v1。它由 repository-reviewed snapshot 在 Astro build-time 產生,公開 response 必須符合 `src/types/public-api.ts`,不得包含 crawler、private source adapter、internal confidence rules 或未公開候選資料。v1.40.0 將 2019 春季擴至 27 套 TV 作品(含電視短篇)與 63 筆 OP/ED,延續既有 endpoint、結構化歌曲來源與可選 `coverageNote`,沒有新增 runtime API。未核對繁中來源的作品不生成譯名來源項目;既有來源 ledger 與 API 欄位保持相容。v1.29.0 起,《拳願阿修羅 Part 2》的 Netflix 配信版為 OP1/ED1,2020 電視播出版為 OP2/ED2,官方影片隨歌曲版本對應;作品 URL 不變。 +本文件定義同源靜態 JSON API v1。它由 repository-reviewed snapshot 在 Astro build-time 產生,公開 response 必須符合 `src/types/public-api.ts`,不得包含 crawler、private source adapter、internal confidence rules 或未公開候選資料。v1.40.0 將 2019 春季擴至 27 套 TV 作品(含電視短篇)與 63 筆 OP/ED,延續既有 endpoint、結構化歌曲來源與可選 `coverageNote`,沒有新增 runtime API。未核對繁中來源的作品不生成譯名來源項目;既有來源 ledger 與 API 欄位保持相容。v1.40.1 補上《Fairy gone》春季歌曲的個別演唱與製作署名及核對來源,沿用既有 credits 與 sources 欄位。v1.29.0 起,《拳願阿修羅 Part 2》的 Netflix 配信版為 OP1/ED1,2020 電視播出版為 OP2/ED2,官方影片隨歌曲版本對應;作品 URL 不變。 ## Build-time integration diff --git a/docs/PROJECT_PLAN.md b/docs/PROJECT_PLAN.md index b017997..d06162c 100644 --- a/docs/PROJECT_PLAN.md +++ b/docs/PROJECT_PLAN.md @@ -68,6 +68,7 @@ - **v1.39.0 source**:2019 春季增加《八月的棒球甜心》、《超可動女孩 1/6》與《八十龜醬觀察日記》,共 3 套 TV 作品(含電視短篇)、4 筆 OP/ED 與 2 筆官方影片 metadata;保留四人翻唱署名、數位先行發行日與歌曲待補充狀態,春季共 26 套作品與 59 筆歌曲;發布證據見對應 GitHub Release。 - **v1.39.1 source**:歌曲卡片合併與藝人欄完全一致的完整合唱名單;保留個別演唱者、非演唱職務、來源、搜尋與全部 API 資料,並支援無 JavaScript 閱讀;發布證據見對應 GitHub Release。 - **v1.40.0 source**:2019 春季增加《YU-NO 在這世界盡頭詠唱愛的少女》TV 版,新增 4 筆 OP/ED 與 2 筆官方短版 MV metadata;前後篇演唱者、完整製作署名與不同發行版本分開核對,春季共 27 套作品與 63 筆歌曲;發布證據見對應 GitHub Release。 +- **v1.40.1 source**:補上《Fairy gone》春季 OP/ED 的個別演唱者、作曲、編曲及 ED 作詞,保留團體名義、發行日期與影片版本;逐曲署名可由創作者搜尋找到,來源包括創作者官方、播出機構及藝人訪談,既有其他作品與歌曲不變;發布證據見對應 GitHub Release。 - 正式網站使用 custom domain;非正式 Cloudflare hostname 不在公開文檔記錄; - 私有 backend、資料庫、crawler、帳戶設定、憑證與內部規則不屬於本公開 repository。 diff --git a/docs/QA_V1_40_1_FAIRY_GONE_CREDITS.md b/docs/QA_V1_40_1_FAIRY_GONE_CREDITS.md new file mode 100644 index 0000000..c22922c --- /dev/null +++ b/docs/QA_V1_40_1_FAIRY_GONE_CREDITS.md @@ -0,0 +1,25 @@ +# v1.40.1|Fairy gone 逐曲署名 QA + +本記錄描述 repository source 與本機驗證;正式部署須在固定版本合併後獨立驗收。 + +## 改動範圍 + +- 補上《Fairy gone》春季 OP「KNOCK on the CORE」的 Ayaka Tachibana、AIJ 演唱署名,以及 ED「Ash-like Snow」的 NIKIIE 演唱署名;團體名義維持 `(K)NoW_NAME`。 +- 依創作者所屬公司 2019 年 4 月單曲履歷,補上兩首歌的宮崎誠作曲、編曲及 ED 的 eNu 作詞;OP 作詞未有足夠第一方證據,沒有推定。 +- 個別演唱者依 Radio NIKKEI 播出資料與 mora 藝人訪談逐曲核對;保留來源語言、角色及 2026-09-07 核對日期。 +- 發行日期、TV Size 說明、官方影片、作品識別及秋季第二部分不變;沒有新增媒體、圖片或 API 欄位。 + +## 本機驗收 + +- `npm run catalog:check`:7 個檔案、200 項通過。 +- `CI=true npm run check`:公開邊界、依賴稽核、Astro check、298 項單元測試、build、Cloudflare dry-run 及 33 項 Playwright 測試通過;`npm audit --audit-level=low` 為 0 項漏洞。 +- 建置產生 1,988 個 HTML 頁面,3,975 個公開產物通過邊界檢查;Wrangler 讀取 5,968 個 Static Assets,0 application bindings。 +- 對照 v1.40.0,1,973 份既有 API JSON 逐位元相同;只更新 `fairy-gone` 作品檔案的歌曲署名與來源,沒有新增或移除檔案。 +- 創作者搜尋涵蓋個別演唱者、作詞、作曲及 OP/ED 篩選;未參與對應演唱的團體成員不會因本次署名更新而出現在該歌曲的搜尋結果。 +- Chromium 1280px 與 390×844 核對兩位不同演唱者的搜尋結果及兩首歌曲卡片,共 8 個畫面;完整署名、日期與 9 個來源連結可讀,沒有水平溢出、頁面程式或 CSP 錯誤、錯誤覆蓋層、iframe 或未經操作的 YouTube 媒體連線。 +- 既有季度分組、導覽、影片鍵盤同意、無 JavaScript、離線、404 與錯誤狀態回歸通過。 +- 未驗證實際 YouTube 播放、實體裝置、Safari、Firefox、Lighthouse/Core Web Vitals。 + +## 發布界線 + +本機與 GitHub CI 通過不等於正式部署完成。發布時須核對固定 SHA、Workers Builds 正式部署與流量、live routes、headers、API 及產物內容;對應 GitHub Release 保留正式驗收摘要。 diff --git a/package-lock.json b/package-lock.json index 1332831..4aec98a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "anisonary", - "version": "1.40.0", + "version": "1.40.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "anisonary", - "version": "1.40.0", + "version": "1.40.1", "license": "SEE LICENSE IN LICENSING.md", "dependencies": { "@astrojs/sitemap": "^3.7.3", diff --git a/package.json b/package.json index d03cde5..e72bd27 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "anisonary", - "version": "1.40.0", + "version": "1.40.1", "private": true, "license": "SEE LICENSE IN LICENSING.md", "type": "module", diff --git a/src/data/curated-seeds/2019/spring.ts b/src/data/curated-seeds/2019/spring.ts index a7a38f2..b35be9f 100644 --- a/src/data/curated-seeds/2019/spring.ts +++ b/src/data/curated-seeds/2019/spring.ts @@ -830,7 +830,12 @@ const curated2019SpringSeedRows: Spring2019SeedInput[] = [ versionLabel: "TV Size 另行配信;單曲版:2019-04-24", youtubeUrl: "https://www.youtube.com/watch?v=B8BAqO-p9LU", releaseDate: "2019-04-24", - credits: [] + credits: [ + { name: "Ayaka Tachibana", role: "vocals" }, + { name: "AIJ", role: "vocals" }, + { name: "宮崎誠", role: "composition" }, + { name: "宮崎誠", role: "arrangement" } + ] }, { type: "ED", @@ -840,7 +845,12 @@ const curated2019SpringSeedRows: Spring2019SeedInput[] = [ versionLabel: "TV Size 另行配信;單曲版:2019-04-24", youtubeUrl: "https://www.youtube.com/watch?v=ee855kKPujc", releaseDate: "2019-04-24", - credits: [] + credits: [ + { name: "NIKIIE", role: "vocals" }, + { name: "eNu", role: "lyrics" }, + { name: "宮崎誠", role: "composition" }, + { name: "宮崎誠", role: "arrangement" } + ] } ] }, diff --git a/src/data/curated-theme-sources/2019/spring.ts b/src/data/curated-theme-sources/2019/spring.ts index 97d2ace..905362e 100644 --- a/src/data/curated-theme-sources/2019/spring.ts +++ b/src/data/curated-theme-sources/2019/spring.ts @@ -116,12 +116,16 @@ const firstPartyUrlsByTheme: Readonly> = { "107418:OP:1": [ "https://www.pa-works.jp/works/fairy-gone/", "https://www.fairygone.com/sp/music/", - "https://www.fairygone.com/sp/news/news_0407_01.html" + "https://www.fairygone.com/sp/news/news_0407_01.html", + "https://www.verygoo.jp/works/202003-201904.php", + "https://www.radionikkei.jp/kodawarisetlist/7317.html" ], "107418:ED:1": [ "https://www.pa-works.jp/works/fairy-gone/", "https://www.fairygone.com/sp/music/", - "https://www.fairygone.com/sp/news/news_0407_01.html" + "https://www.fairygone.com/sp/news/news_0407_01.html", + "https://www.verygoo.jp/works/202003-201904.php", + "https://www.radionikkei.jp/kodawarisetlist/7317.html" ], "100112:OP:1": [ "https://kenja-no-mago.jp/products/", @@ -252,6 +256,8 @@ const sourceLabelsByUrl: Readonly> = { "https://www.pa-works.jp/works/fairy-gone/": "P.A.WORKS 官方:第一季 OP/ED 用途與藝人", "https://www.fairygone.com/sp/music/": "動畫官方:第一季 OP/ED 單曲發行資料", "https://www.fairygone.com/sp/news/news_0407_01.html": "動畫官方:第一季 OP/ED 映像與 TV Size 配信公告", + "https://www.verygoo.jp/works/202003-201904.php": "VERY GOO 創作者官方:2019 年 4 月單曲的作曲、編曲與 ED 作詞署名", + "https://www.radionikkei.jp/kodawarisetlist/7317.html": "Radio NIKKEI 播出機構:兩首歌曲的個別演唱者", "https://iris.dive2ent.com/discography/detail.php?id=1016413": "i☆Ris 官方:OP 單曲與發行日期", "https://iris.dive2ent.com/discography/detail.php?id=1017265": "i☆Ris 官方:OP 收錄與製作署名核對", "https://avex.jp/yoshinanami/discography/detail.php?id=1016506": "avex 官方:ED 歌曲、發行日期與製作資料", @@ -318,6 +324,12 @@ export const curated2019SpringThemeSources: CuratedThemeSourceOverrideMap = Obje language: "ja" as const, role: "cross_check" as const }] : []), + ...(seed.anilistId === 107418 ? [{ + label: "mora 藝人訪談:OP 對唱與 ED 獨唱的逐曲交叉核對", + url: "https://mora.jp/topics/interview/knowname_fairygone/", + language: "ja" as const, + role: "cross_check" as const + }] : []), crossCheck ]; return [key, sources]; diff --git a/tests/e2e/phase1.spec.ts b/tests/e2e/phase1.spec.ts index a8d8810..d76bccc 100644 --- a/tests/e2e/phase1.spec.ts +++ b/tests/e2e/phase1.spec.ts @@ -1262,8 +1262,10 @@ test("supernatural spring songs preserve co-writers, TV edits and distinct Fairy await page.goto("/anime/fairy-gone/", { waitUntil: "domcontentloaded" }); await expect(page.locator(".theme-card h3")).toHaveText(["KNOCK on the CORE", "Ash-like Snow"]); - await expect(page.locator(".theme-card__credits dd")).toHaveText(["待確認", "待確認"]); + await expect(page.locator("#theme-fairy-gone-op-1 .theme-card__credits dd")).toHaveText(["Ayaka Tachibana、AIJ", "宮崎誠", "宮崎誠"]); + await expect(page.locator("#theme-fairy-gone-ed-1 .theme-card__credits dd")).toHaveText(["NIKIIE", "eNu", "宮崎誠", "宮崎誠"]); await expect(page.locator("#theme-fairy-gone-ed-1")).toContainText("TV Size 另行配信;單曲版:2019-04-24"); + await expect(page.locator("#theme-fairy-gone-op-1").getByRole("link", { name: /VERY GOO/ })).toHaveAttribute("href", "https://www.verygoo.jp/works/202003-201904.php"); expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); await page.goto("/anime/fairy-gone-2/", { waitUntil: "domcontentloaded" }); await expect(page.locator(".theme-card h3")).toHaveText(["STILL STANDING", "Stay Gold"]); @@ -1272,6 +1274,13 @@ test("supernatural spring songs preserve co-writers, TV edits and distinct Fairy await expect(page.locator("#theme-sarazanmai-op-1 .theme-card__credits div").filter({ hasText: "谷口鮪" })).toHaveCount(3); await expect(page.locator("iframe")).toHaveCount(0); expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); + await page.goto("/search/"); + for (const [label, value] of [["年份", "2019"], ["季度", "spring"], ["搜尋範圍", "creators"], ["歌曲用途", "ED"]]) { + await page.getByLabel(label!, { exact: true }).selectOption(value!); + } + await page.getByRole("searchbox", { name: "搜尋動畫、歌曲或創作者" }).fill("NIKIIE"); + await page.getByRole("link", { name: "Ash-like Snow", exact: true }).click(); + await expect(page).toHaveURL(/\/anime\/fairy-gone\/#theme-fairy-gone-ed-1$/); } expect(mediaRequests).toEqual([]); await page.goto("/anime/fairy-gone/", { waitUntil: "domcontentloaded" }); diff --git a/tests/unit/curated-2019-spring.test.ts b/tests/unit/curated-2019-spring.test.ts index 31c411a..9be70c2 100644 --- a/tests/unit/curated-2019-spring.test.ts +++ b/tests/unit/curated-2019-spring.test.ts @@ -594,8 +594,17 @@ describe("2019 spring reviewed TV catalogue", () => { const fairy = anime(107418); expect(fairy.themes.map(({ titleJa }) => titleJa)).toEqual(["KNOCK on the CORE", "Ash-like Snow"]); expect(anime(109562).themes.map(({ titleJa }) => titleJa)).toEqual(["STILL STANDING", "Stay Gold"]); + expect(fairy.themes.map(({ credits }) => credits.filter(({ role }) => role === "vocals").map(({ name }) => name))) + .toEqual([["Ayaka Tachibana", "AIJ"], ["NIKIIE"]]); + expect(fairy.themes[0]?.credits.some(({ role }) => role === "lyrics")).toBe(false); + expect(fairy.themes[1]?.credits).toContainEqual({ name: "eNu", role: "lyrics" }); for (const theme of fairy.themes) { - expect(theme.credits).toEqual([]); + expect(theme.artistDisplayName).toBe("(K)NoW_NAME"); + expect(theme.credits).toContainEqual({ name: "宮崎誠", role: "composition" }); + expect(theme.credits).toContainEqual({ name: "宮崎誠", role: "arrangement" }); + expect(theme.sources).toContainEqual(expect.objectContaining({ url: "https://www.verygoo.jp/works/202003-201904.php", role: "first_party" })); + expect(theme.sources).toContainEqual(expect.objectContaining({ url: "https://www.radionikkei.jp/kodawarisetlist/7317.html", role: "first_party" })); + expect(theme.sources).toContainEqual(expect.objectContaining({ url: "https://mora.jp/topics/interview/knowname_fairygone/", role: "cross_check" })); expect(theme.releaseDate).toBe("2019-04-24"); expect(theme.versionLabel).toBe("TV Size 另行配信;單曲版:2019-04-24"); } @@ -607,4 +616,19 @@ describe("2019 spring reviewed TV catalogue", () => { expect(fairyResults.map(({ anime: item }) => item.slug)).toEqual(["fairy-gone"]); expect(fairyResults[0]?.themes.map(({ titleJa }) => titleJa)).toEqual(["Ash-like Snow"]); }); + + it("finds Fairy gone songs by reviewed individual credits without assigning the whole group's vocals to both songs", async () => { + const index = buildCatalogSearchIndex((await loadCatalogSearchData(new CuratedProvider(), true)).entries); + for (const [query, type, expected] of [ + ["Ayaka Tachibana", "OP", ["KNOCK on the CORE"]], ["Ayaka Tachibana", "ED", []], + ["AIJ", "OP", ["KNOCK on the CORE"]], ["AIJ", "ED", []], + ["NIKIIE", "OP", []], ["NIKIIE", "ED", ["Ash-like Snow"]], + ["eNu", "ED", ["Ash-like Snow"]], ["宮崎誠", "OP", ["KNOCK on the CORE"]], + ["宮崎誠", "ED", ["Ash-like Snow"]] + ] as const) { + const result = searchCatalog(index, { query, type, scope: "creators", year: "2019", quarter: "spring" }) + .find(({ anime: item }) => item.slug === "fairy-gone"); + expect(result?.themes.map(({ titleJa }) => titleJa) ?? []).toEqual(expected); + } + }); });