Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ 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 並稽核開發依賴。

Production build 會從最終 HTML 自動產生 hash-based Content Security Policy。政策不使用 `unsafe-inline` 或 `unsafe-eval`,禁止 inline event/style attributes,只開放同源資產、已核對的海報來源及使用者啟動後的 YouTube privacy-enhanced iframe。任何未批准的 media origin 會令 build fail closed。

| 可用性 / 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

Expand Down
2 changes: 1 addition & 1 deletion docs/API_HANDOFF.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
1 change: 1 addition & 0 deletions docs/PROJECT_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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。

Expand Down
25 changes: 25 additions & 0 deletions docs/QA_V1_40_1_FAIRY_GONE_CREDITS.md
Original file line number Diff line number Diff line change
@@ -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 保留正式驗收摘要。
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "anisonary",
"version": "1.40.0",
"version": "1.40.1",
"private": true,
"license": "SEE LICENSE IN LICENSING.md",
"type": "module",
Expand Down
14 changes: 12 additions & 2 deletions src/data/curated-seeds/2019/spring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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" }
]
}
]
},
Expand Down
16 changes: 14 additions & 2 deletions src/data/curated-theme-sources/2019/spring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,16 @@ const firstPartyUrlsByTheme: Readonly<Record<string, readonly string[]>> = {
"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/",
Expand Down Expand Up @@ -252,6 +256,8 @@ const sourceLabelsByUrl: Readonly<Record<string, string>> = {
"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 歌曲、發行日期與製作資料",
Expand Down Expand Up @@ -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];
Expand Down
11 changes: 10 additions & 1 deletion tests/e2e/phase1.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"]);
Expand All @@ -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" });
Expand Down
26 changes: 25 additions & 1 deletion tests/unit/curated-2019-spring.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand All @@ -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);
}
});
});