Add Greatest of All Time manga dashboard
This commit is contained in:
1 parent
c8dd46d0fb
commit
5df94db9a3
16 files changed
+2717
-1
No files matched your search
@@ -9,6 +9,8 @@
|
|||||||
!reading-analytics.js
|
!reading-analytics.js
|
||||||
!public/
|
!public/
|
||||||
!public/**
|
!public/**
|
||||||
|
!greatest/
|
||||||
|
!greatest/**
|
||||||
!runtime/
|
!runtime/
|
||||||
!runtime/**
|
!runtime/**
|
||||||
!sentinel/
|
!sentinel/
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ ARG VCS_REF=development
|
|||||||
LABEL org.opencontainers.image.title="MangaCircus" org.opencontainers.image.revision=$VCS_REF
|
LABEL org.opencontainers.image.title="MangaCircus" org.opencontainers.image.revision=$VCS_REF
|
||||||
COPY package.json server.js library.js reading-direction.js chapter-identity.js reading-analytics.js catalog.js ./
|
COPY package.json server.js library.js reading-direction.js chapter-identity.js reading-analytics.js catalog.js ./
|
||||||
COPY public ./public
|
COPY public ./public
|
||||||
|
COPY greatest ./greatest
|
||||||
COPY tools ./tools
|
COPY tools ./tools
|
||||||
COPY scraper ./scraper
|
COPY scraper ./scraper
|
||||||
COPY sentinel ./sentinel
|
COPY sentinel ./sentinel
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ A no-frills, self-hosted manga / comic / webtoon reader.
|
|||||||
- Reads the manga straight from a folder tree. The same folders stay readable in any normal picture viewer.
|
- Reads the manga straight from a folder tree. The same folders stay readable in any normal picture viewer.
|
||||||
- Accounts with per-user reading progress, view settings, an opt-in Active Reading Clock, a private Reading Story and "new chapter" notifications.
|
- Accounts with per-user reading progress, view settings, an opt-in Active Reading Clock, a private Reading Story and "new chapter" notifications.
|
||||||
- Reader with three view modes (**Flow**, **Page**, **Book**), a page zoom that leaves the controls alone, and tap zones for mobile.
|
- Reader with three view modes (**Flow**, **Page**, **Book**), a page zoom that leaves the controls alone, and tap zones for mobile.
|
||||||
|
- A separate **Greatest of All Time** manga-history dashboard, with five-year periods, per-user wish ratings, community ranking, and local-library badges. Its curated titles do not enter the download queue.
|
||||||
|
|
||||||
## Run it
|
## Run it
|
||||||
|
|
||||||
@@ -29,6 +30,7 @@ The current Windows installation uses Docker Compose; its old `MangaCircus Super
|
|||||||
Scheduled Task is disabled. Do not run a native duplicate writer. See
|
Scheduled Task is disabled. Do not run a native duplicate writer. See
|
||||||
[docs/local-deployment.md](docs/local-deployment.md) for its observed layout, health
|
[docs/local-deployment.md](docs/local-deployment.md) for its observed layout, health
|
||||||
checks, restart procedure, and recovery point.
|
checks, restart procedure, and recovery point.
|
||||||
|
See [Greatest of All Time](docs/greatest.md) for catalog ownership, voting, and metadata refresh.
|
||||||
|
|
||||||
### Docker
|
### Docker
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Shijō Saikō no Manga — Greatest of All Time
|
||||||
|
|
||||||
|
`greatest.html` is a second, single-page dashboard. The Japanese title is **史上最高の漫画**, romanized **Shijō Saikō no Manga** (“greatest manga of all time”). It displays the [research reading map](manga-reading-map-2026-10-09.md) as ranked five-year cohorts from before 1970 through 2025–2026. Each period keeps its own separator and tile grid.
|
||||||
|
|
||||||
|
## Separation and data ownership
|
||||||
|
|
||||||
|
- `greatest/catalog.json` is a versioned, read-only editorial seed in the application image. Entries have stable IDs, debut year, cohort, curator rank, rationale, official source lead, and optional AniList cover/chapter/status metadata. It is **not** part of `library/`, scraper queue, or `data/catalog.json` collections.
|
||||||
|
- `greatest/index.js` validates the seed and performs only exact title/alias matching against the existing library index. A match adds the local cover, chapter count, status and **In library** badge; it never moves or downloads a title.
|
||||||
|
- `data/db.json` holds `greatestRatings[userName][entryId]`. Each account can select `flame`, `good`, `neutral`, or `skip`, or click its current choice to clear it. Existing chapter-version votes are separate. Deleting a user removes their ratings. Include `data/db.json` in backups as before.
|
||||||
|
- The only write route is authenticated `POST /api/greatest/rating` with `{id,rating}` (`null` clears). Authenticated `GET /api/greatest` returns the catalog, per-user choice, valid-user community counts, score, and local-library match. Unknown IDs/ratings are rejected. There is no admin-only gate: every account may vote.
|
||||||
|
|
||||||
|
Within each cohort, **Rank by my vote** sorts flame → good → neutral → skull → unrated; **Rank by everyone** uses `3×flames + thumbs-up − 2×skulls`, with curator rank as the tie-breaker. These are preference scores, not independent critical ratings. The quick chips filter by *your* ratings (or in-library status), even when community sorting is active. The popup shows the full rationale, metadata, official source link, community counts, and a library link where available.
|
||||||
|
|
||||||
|
## Covers and metadata
|
||||||
|
|
||||||
|
The one-time seed is curated from the research document. `node tools/enrich-greatest-catalog.js` is an **optional manual** public-AniList metadata refresh, paced at one request every 1.5 seconds. It only accepts matching manga title and **original serialization year**, leaving uncertain results blank rather than using a sequel or remake cover. The server makes **no AniList calls at runtime**. Browser covers are lazy-loaded from AniList's CDN; a title-letter card remains if a cover is missing or fails. Local library covers take precedence.
|
||||||
|
|
||||||
|
Public catalog chapter counts are often absent; the UI says “Chapters unknown” rather than inventing one. For owned titles, the local library count wins. Status comes from AniList's releasing/finished flag or local library metadata, and may be unknown. Official source links may be publisher editions, chapter services, or an award/info lead; they do not imply all chapters are available in every country. No unverified hoster coverage is claimed.
|
||||||
|
|
||||||
|
## Deployment and QA
|
||||||
|
|
||||||
|
The Docker image now copies `greatest/` as app code; no new Compose service, volume, port, secret, or migration is needed. Rebuild/recreate **reader only** to pick up catalog/UI changes. The scraper and sentinel do not need restarting. Existing `data/db.json` accepts the new `greatestRatings` object without migration.
|
||||||
|
|
||||||
|
Run `npm run test:reader` for catalog/API tests and `node tools/test-greatest-ui.js` for disposable headless-browser QA. The UI fixture blocks remote covers and never contacts the scraper. Manual catalog edits should preserve stable IDs once anyone has voted; changing a title's ID strands its votes.
|
||||||
@@ -0,0 +1,2202 @@
|
|||||||
|
{
|
||||||
|
"schema": 1,
|
||||||
|
"updatedAt": "2026-10-09",
|
||||||
|
"title": "Shijō Saikō no Manga",
|
||||||
|
"periods": [
|
||||||
|
"Before 1970",
|
||||||
|
"1970–1974",
|
||||||
|
"1975–1979",
|
||||||
|
"1980–1984",
|
||||||
|
"1985–1989",
|
||||||
|
"1990–1994",
|
||||||
|
"1995–1999",
|
||||||
|
"2000–2004",
|
||||||
|
"2005–2009",
|
||||||
|
"2010–2014",
|
||||||
|
"2015–2019",
|
||||||
|
"2020–2024",
|
||||||
|
"2025–2026"
|
||||||
|
],
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"id": "astro-boy-1952",
|
||||||
|
"title": "Astro Boy",
|
||||||
|
"year": 1952,
|
||||||
|
"period": "Before 1970",
|
||||||
|
"periodOrder": 0,
|
||||||
|
"rank": 1,
|
||||||
|
"note": "Humane robot science fiction that made long-form adventure manga feel modern; a foundation for the *Pluto* branch later below.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://www.darkhorse.com/Books/25-825/Astro-Boy-Omnibus-Volume-1-TPB"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx30728-deU0SZKHwSp5.png",
|
||||||
|
"chapters": 233,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 30728,
|
||||||
|
"url": "https://anilist.co/manga/30728"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "phoenix-1954",
|
||||||
|
"title": "Phoenix",
|
||||||
|
"year": 1954,
|
||||||
|
"period": "Before 1970",
|
||||||
|
"periodOrder": 0,
|
||||||
|
"rank": 2,
|
||||||
|
"note": "Tezuka's time-spanning meditation on life, death, and recurrence; the most ambitious early match to your philosophical sci-fi taste.",
|
||||||
|
"source": {
|
||||||
|
"kind": "info",
|
||||||
|
"url": "https://tezukaosamu.net/en/manga/656.html"
|
||||||
|
},
|
||||||
|
"cover": null,
|
||||||
|
"chapters": null,
|
||||||
|
"status": "unknown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ashita-no-joe-1968",
|
||||||
|
"title": "Ashita no Joe",
|
||||||
|
"year": 1968,
|
||||||
|
"period": "Before 1970",
|
||||||
|
"periodOrder": 0,
|
||||||
|
"rank": 3,
|
||||||
|
"note": "Boxing as class struggle, self-destruction, and national cultural memory rather than a neat victory ladder.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://kodansha.us/series/ashita-no-joe-fighting-for-tomorrow/"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx31303-l21HJn9AXl7w.jpg",
|
||||||
|
"chapters": 171,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 31303,
|
||||||
|
"url": "https://anilist.co/manga/31303"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "princess-knight-1953",
|
||||||
|
"title": "Princess Knight",
|
||||||
|
"year": 1953,
|
||||||
|
"period": "Before 1970",
|
||||||
|
"periodOrder": 0,
|
||||||
|
"rank": 4,
|
||||||
|
"note": "A crucial early story-driven shōjo fantasy; historically important even if its gender treatment feels of its time.",
|
||||||
|
"source": {
|
||||||
|
"kind": "info",
|
||||||
|
"url": "https://tezukaosamu.net/en/manga/559.html"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/b31676-ReKRBqwbdml1.jpg",
|
||||||
|
"chapters": 27,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 31676,
|
||||||
|
"url": "https://anilist.co/manga/31676"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "golgo-13-1968",
|
||||||
|
"title": "Golgo 13",
|
||||||
|
"year": 1968,
|
||||||
|
"period": "Before 1970",
|
||||||
|
"periodOrder": 0,
|
||||||
|
"rank": 5,
|
||||||
|
"note": "The hard-boiled assassin serial as a decades-long political/time-capsule form.",
|
||||||
|
"source": {
|
||||||
|
"kind": "read",
|
||||||
|
"url": "https://www.viz.com/vizmanga/chapters/golgo-13"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/nx31298-Cik2Ff9Ip3MD.jpg",
|
||||||
|
"chapters": null,
|
||||||
|
"status": "ongoing",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 31298,
|
||||||
|
"url": "https://anilist.co/manga/31298"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "dororo-1967",
|
||||||
|
"title": "Dororo",
|
||||||
|
"year": 1967,
|
||||||
|
"period": "Before 1970",
|
||||||
|
"periodOrder": 0,
|
||||||
|
"rank": 6,
|
||||||
|
"note": "Body-horror quest through a demon-infested feudal Japan; an unusually direct ancestor of your dark-fantasy favorites.",
|
||||||
|
"source": {
|
||||||
|
"kind": "info",
|
||||||
|
"url": "https://tezukaosamu.net/en/manga/310.html"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx31658-xZFgMcIzhbB4.png",
|
||||||
|
"chapters": 19,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 31658,
|
||||||
|
"url": "https://anilist.co/manga/31658"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "the-legend-of-kamui-kamui-den-1964",
|
||||||
|
"title": "The Legend of Kamui / Kamui Den",
|
||||||
|
"year": 1964,
|
||||||
|
"period": "Before 1970",
|
||||||
|
"periodOrder": 0,
|
||||||
|
"rank": 7,
|
||||||
|
"note": "Dense ninja history with caste, rebellion, and social realism; rewarding but a demanding classic.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://drawnandquarterly.com/books/the-legend-of-kamui/"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx47839-3sf0J3dOnN2O.png",
|
||||||
|
"chapters": 74,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 47839,
|
||||||
|
"url": "https://anilist.co/manga/47839"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "kitaro-gegege-no-kitaro-1960",
|
||||||
|
"title": "Kitarō / GeGeGe no Kitarō",
|
||||||
|
"year": 1960,
|
||||||
|
"period": "Before 1970",
|
||||||
|
"periodOrder": 0,
|
||||||
|
"rank": 8,
|
||||||
|
"note": "Yōkai folklore recast as popular manga; a useful bridge to later Japanese horror. English editions are selections from different incarnations.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://drawnandquarterly.com/books/kitaros-yokai-battles/"
|
||||||
|
},
|
||||||
|
"cover": null,
|
||||||
|
"chapters": null,
|
||||||
|
"status": "unknown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "lone-wolf-and-cub-1970",
|
||||||
|
"title": "Lone Wolf and Cub",
|
||||||
|
"year": 1970,
|
||||||
|
"period": "1970–1974",
|
||||||
|
"periodOrder": 1,
|
||||||
|
"rank": 1,
|
||||||
|
"note": "An austere father–son samurai epic whose visual storytelling still feels cinematic.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://www.darkhorse.com/Books/40-092/Lone-Wolf-and-Cub-Vol-1-The-Assassins-Road-TPB"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx30904-KRN1WE3ZU3vl.jpg",
|
||||||
|
"chapters": 142,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 30904,
|
||||||
|
"url": "https://anilist.co/manga/30904"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "the-rose-of-versailles-1972",
|
||||||
|
"title": "The Rose of Versailles",
|
||||||
|
"year": 1972,
|
||||||
|
"period": "1970–1974",
|
||||||
|
"periodOrder": 1,
|
||||||
|
"rank": 2,
|
||||||
|
"note": "French Revolution melodrama that radically expanded what historical shōjo could do.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://store.udonentertainment.com/collections/the-rose-of-versailles"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx30660-hBFkOt2Ab2Ti.jpg",
|
||||||
|
"chapters": 85,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 30660,
|
||||||
|
"url": "https://anilist.co/manga/30660"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "devilman-1972",
|
||||||
|
"title": "Devilman",
|
||||||
|
"year": 1972,
|
||||||
|
"period": "1970–1974",
|
||||||
|
"periodOrder": 1,
|
||||||
|
"rank": 3,
|
||||||
|
"note": "Apocalyptic horror, moral collapse, and a major ancestor of modern dark fantasy.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://sevenseasentertainment.com/series/devilman-the-classic-collection/"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx31474-R4x7qkjUfBw6.jpg",
|
||||||
|
"chapters": 53,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 31474,
|
||||||
|
"url": "https://anilist.co/manga/31474"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "buddha-1972",
|
||||||
|
"title": "Buddha",
|
||||||
|
"year": 1972,
|
||||||
|
"period": "1970–1974",
|
||||||
|
"periodOrder": 1,
|
||||||
|
"rank": 4,
|
||||||
|
"note": "Tezuka's compassionate, irreverent philosophical biography, with much more grit than the premise suggests.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://kodansha.us/series/buddha/"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx31111-ldPYMNd3PvHa.png",
|
||||||
|
"chapters": 69,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 31111,
|
||||||
|
"url": "https://anilist.co/manga/31111"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "black-jack-1973",
|
||||||
|
"title": "Black Jack",
|
||||||
|
"year": 1973,
|
||||||
|
"period": "1970–1974",
|
||||||
|
"periodOrder": 1,
|
||||||
|
"rank": 5,
|
||||||
|
"note": "Brilliant episodic medical ethics built around an outlaw surgeon; ideal if you like tight moral dilemmas.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://kodansha.us/series/black-jack/"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/nx31600-CCA5TUKJBQJh.jpg",
|
||||||
|
"chapters": 243,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 31600,
|
||||||
|
"url": "https://anilist.co/manga/31600"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "barefoot-gen-1973",
|
||||||
|
"title": "Barefoot Gen",
|
||||||
|
"year": 1973,
|
||||||
|
"period": "1970–1974",
|
||||||
|
"periodOrder": 1,
|
||||||
|
"rank": 6,
|
||||||
|
"note": "Essential, harrowing Hiroshima survivor testimony in comics form.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://lastgasp.com/collections/barefoot-gen"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx33281-2NXB8uHRxpI3.png",
|
||||||
|
"chapters": 54,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 33281,
|
||||||
|
"url": "https://anilist.co/manga/33281"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "the-poe-clan-1972",
|
||||||
|
"title": "The Poe Clan",
|
||||||
|
"year": 1972,
|
||||||
|
"period": "1970–1974",
|
||||||
|
"periodOrder": 1,
|
||||||
|
"rank": 7,
|
||||||
|
"note": "Moto Hagio's melancholy vampire mosaic; a beautiful source for the gothic and psychological side of manga.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://www.fantagraphics.com/products/the-complete-poe-clan-vols-1-2"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx34305-ul28wvP0lhjv.jpg",
|
||||||
|
"chapters": 26,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 34305,
|
||||||
|
"url": "https://anilist.co/manga/34305"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "the-heart-of-thomas-1974",
|
||||||
|
"title": "The Heart of Thomas",
|
||||||
|
"year": 1974,
|
||||||
|
"period": "1970–1974",
|
||||||
|
"periodOrder": 1,
|
||||||
|
"rank": 8,
|
||||||
|
"note": "A school tragedy that helped establish psychologically intricate boys'-love/shōjo storytelling.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://www.fantagraphics.com/products/heart-of-thomas"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx39152-QcXrecYJwsEQ.jpg",
|
||||||
|
"chapters": 40,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 39152,
|
||||||
|
"url": "https://anilist.co/manga/39152"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "the-glass-mask-1975",
|
||||||
|
"title": "The Glass Mask",
|
||||||
|
"year": 1975,
|
||||||
|
"period": "1975–1979",
|
||||||
|
"periodOrder": 2,
|
||||||
|
"rank": 1,
|
||||||
|
"note": "Legendary acting rivalry and unusually intense long-form character growth;",
|
||||||
|
"source": null,
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx31469-xYQ8kWQynM7n.png",
|
||||||
|
"chapters": null,
|
||||||
|
"status": "ongoing",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 31469,
|
||||||
|
"url": "https://anilist.co/manga/31469"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "galaxy-express-999-1977",
|
||||||
|
"title": "Galaxy Express 999",
|
||||||
|
"year": 1977,
|
||||||
|
"period": "1975–1979",
|
||||||
|
"periodOrder": 2,
|
||||||
|
"rank": 2,
|
||||||
|
"note": "Melancholy train-through-space fables about bodies, technology, and mortality;",
|
||||||
|
"source": null,
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/nx39705-uxZAOISKjKgB.jpg",
|
||||||
|
"chapters": 91,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 39705,
|
||||||
|
"url": "https://anilist.co/manga/39705"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "urusei-yatsura-1978",
|
||||||
|
"title": "Urusei Yatsura",
|
||||||
|
"year": 1978,
|
||||||
|
"period": "1975–1979",
|
||||||
|
"periodOrder": 2,
|
||||||
|
"rank": 3,
|
||||||
|
"note": "Rumiko Takahashi's anarchic rom-com machine; hugely influential on ensemble comedy.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://www.viz.com/urusei-yatsura"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx30304-jAQt0fJkMgUd.jpg",
|
||||||
|
"chapters": 366,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 30304,
|
||||||
|
"url": "https://anilist.co/manga/30304"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "captain-harlock-1977",
|
||||||
|
"title": "Captain Harlock",
|
||||||
|
"year": 1977,
|
||||||
|
"period": "1975–1979",
|
||||||
|
"periodOrder": 2,
|
||||||
|
"rank": 4,
|
||||||
|
"note": "Romantic space-pirate resistance and striking Leiji Matsumoto atmosphere.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://sevenseasentertainment.com/series/captain-harlock-the-classic-collection/"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/nx35115-fZzfIJXnDYds.jpg",
|
||||||
|
"chapters": 55,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 35115,
|
||||||
|
"url": "https://anilist.co/manga/35115"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "candy-candy-1975",
|
||||||
|
"title": "Candy Candy",
|
||||||
|
"year": 1975,
|
||||||
|
"period": "1975–1979",
|
||||||
|
"periodOrder": 2,
|
||||||
|
"rank": 5,
|
||||||
|
"note": "A shōjo melodrama landmark with cross-generational popularity;",
|
||||||
|
"source": null,
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/nx31388-JDIwg74gvgt3.jpg",
|
||||||
|
"chapters": 44,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 31388,
|
||||||
|
"url": "https://anilist.co/manga/31388"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "the-poem-of-wind-and-trees-1976",
|
||||||
|
"title": "The Poem of Wind and Trees",
|
||||||
|
"year": 1976,
|
||||||
|
"period": "1975–1979",
|
||||||
|
"periodOrder": 2,
|
||||||
|
"rank": 6,
|
||||||
|
"note": "A historically important, emotionally severe early boys'-love work;",
|
||||||
|
"source": null,
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx31365-ucezaL74sIgK.jpg",
|
||||||
|
"chapters": 139,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 31365,
|
||||||
|
"url": "https://anilist.co/manga/31365"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "kochikame-1976",
|
||||||
|
"title": "Kochikame",
|
||||||
|
"year": 1976,
|
||||||
|
"period": "1975–1979",
|
||||||
|
"periodOrder": 2,
|
||||||
|
"rank": 7,
|
||||||
|
"note": "Everyday police-station comedy whose extraordinary longevity makes it a time capsule; full",
|
||||||
|
"source": null,
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/nx30733-QmyPwBhjbgyX.jpg",
|
||||||
|
"chapters": 1968,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 30733,
|
||||||
|
"url": "https://anilist.co/manga/30733"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "akira-1982",
|
||||||
|
"title": "Akira",
|
||||||
|
"year": 1982,
|
||||||
|
"period": "1980–1984",
|
||||||
|
"periodOrder": 3,
|
||||||
|
"rank": 1,
|
||||||
|
"note": "The definitive urban-collapse/cyberpunk landmark; a particularly strong fit for your *BLAME!* and *Biomega* shelf.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://kodansha.us/series/akira/"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx30664-JJq9tosQvlpW.jpg",
|
||||||
|
"chapters": 120,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 30664,
|
||||||
|
"url": "https://anilist.co/manga/30664"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "nausicaa-of-the-valley-of-the-wind-1982",
|
||||||
|
"title": "Nausicaä of the Valley of the Wind",
|
||||||
|
"year": 1982,
|
||||||
|
"period": "1980–1984",
|
||||||
|
"periodOrder": 3,
|
||||||
|
"rank": 2,
|
||||||
|
"note": "Miyazaki's manga is a far deeper ecological-war epic than its film adaptation.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://www.viz.com/manga-books/manga/nausicaa-of-the-valley-of-the-wind/all"
|
||||||
|
},
|
||||||
|
"cover": null,
|
||||||
|
"chapters": null,
|
||||||
|
"status": "unknown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "dragon-ball-1984",
|
||||||
|
"title": "Dragon Ball",
|
||||||
|
"year": 1984,
|
||||||
|
"period": "1980–1984",
|
||||||
|
"periodOrder": 3,
|
||||||
|
"rank": 3,
|
||||||
|
"note": "The grammar of modern battle shōnen, executed with an unmatched sense of motion and play.",
|
||||||
|
"source": {
|
||||||
|
"kind": "read",
|
||||||
|
"url": "https://mangaplus.shueisha.co.jp/titles/100011"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx30042-4SetGiEbGc9x.jpg",
|
||||||
|
"chapters": 520,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 30042,
|
||||||
|
"url": "https://anilist.co/manga/30042"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fist-of-the-north-star-1983",
|
||||||
|
"title": "Fist of the North Star",
|
||||||
|
"year": 1983,
|
||||||
|
"period": "1980–1984",
|
||||||
|
"periodOrder": 3,
|
||||||
|
"rank": 4,
|
||||||
|
"note": "Post-apocalyptic martial-arts excess that shaped action manga's iconography.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://www.viz.com/fist-of-the-north-star"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx31149-TOwqxOlY1Zs0.jpg",
|
||||||
|
"chapters": 245,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 31149,
|
||||||
|
"url": "https://anilist.co/manga/31149"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "maison-ikkoku-1980",
|
||||||
|
"title": "Maison Ikkoku",
|
||||||
|
"year": 1980,
|
||||||
|
"period": "1980–1984",
|
||||||
|
"periodOrder": 3,
|
||||||
|
"rank": 5,
|
||||||
|
"note": "A surprisingly adult, patient romance/comedy about grief and choosing a future.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://www.viz.com/maison-ikkoku"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx30688-vhFGM9jUcGrU.png",
|
||||||
|
"chapters": 162,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 30688,
|
||||||
|
"url": "https://anilist.co/manga/30688"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "touch-1981",
|
||||||
|
"title": "Touch",
|
||||||
|
"year": 1981,
|
||||||
|
"period": "1980–1984",
|
||||||
|
"periodOrder": 3,
|
||||||
|
"rank": 6,
|
||||||
|
"note": "Mitsuru Adachi's baseball romance: understatement, timing, and a devastating turn.",
|
||||||
|
"source": null,
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx31013-Suolsv3rhJnA.jpg",
|
||||||
|
"chapters": 257,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 31013,
|
||||||
|
"url": "https://anilist.co/manga/31013"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "captain-tsubasa-1981",
|
||||||
|
"title": "Captain Tsubasa",
|
||||||
|
"year": 1981,
|
||||||
|
"period": "1980–1984",
|
||||||
|
"periodOrder": 3,
|
||||||
|
"rank": 7,
|
||||||
|
"note": "Football dreams drawn at full kinetic intensity; foundational sports manga.",
|
||||||
|
"source": {
|
||||||
|
"kind": "read",
|
||||||
|
"url": "https://mangaplus.shueisha.co.jp/titles/200014"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx31789-qiNlHJDMJncz.png",
|
||||||
|
"chapters": 356,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 31789,
|
||||||
|
"url": "https://anilist.co/manga/31789"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "dr-slump-1980",
|
||||||
|
"title": "Dr. Slump",
|
||||||
|
"year": 1980,
|
||||||
|
"period": "1980–1984",
|
||||||
|
"periodOrder": 3,
|
||||||
|
"rank": 8,
|
||||||
|
"note": "Toriyama's wild gag-comic laboratory before *Dragon Ball*.",
|
||||||
|
"source": {
|
||||||
|
"kind": "read",
|
||||||
|
"url": "https://www.viz.com/shonenjump/chapters/dr-slump"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx30796-PFF4n7Y2spz9.png",
|
||||||
|
"chapters": 236,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 30796,
|
||||||
|
"url": "https://anilist.co/manga/30796"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "berserk-1989",
|
||||||
|
"title": "Berserk",
|
||||||
|
"year": 1989,
|
||||||
|
"period": "1985–1989",
|
||||||
|
"periodOrder": 4,
|
||||||
|
"rank": 1,
|
||||||
|
"note": "Monumental dark fantasy, trauma, and page design; already the clearest taste anchor in your library.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://www.darkhorse.com/books/12-705/berserk-volume-1-tpb/"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx30002-Cul4OeN7bYtn.jpg",
|
||||||
|
"chapters": null,
|
||||||
|
"status": "ongoing",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 30002,
|
||||||
|
"url": "https://anilist.co/manga/30002"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "jojo-s-bizarre-adventure-1987",
|
||||||
|
"title": "JoJo's Bizarre Adventure",
|
||||||
|
"year": 1987,
|
||||||
|
"period": "1985–1989",
|
||||||
|
"periodOrder": 4,
|
||||||
|
"rank": 2,
|
||||||
|
"note": "Reinvents its own rules, cast, and visual language across parts; start with Part 1 rather than cataloging *Steel Ball Run* as the debut.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://www.viz.com/jojo-s-bizarre-adventure"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx31630-Fg8JERVKuWB5.jpg",
|
||||||
|
"chapters": 69,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 31630,
|
||||||
|
"url": "https://anilist.co/manga/31630"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "parasyte-1989",
|
||||||
|
"title": "Parasyte",
|
||||||
|
"year": 1989,
|
||||||
|
"period": "1985–1989",
|
||||||
|
"periodOrder": 4,
|
||||||
|
"rank": 3,
|
||||||
|
"note": "Body horror sharpened into an argument about ecology and personhood.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://kodansha.us/series/parasyte/"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx30401-siY6wmwnHFi6.png",
|
||||||
|
"chapters": 64,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 30401,
|
||||||
|
"url": "https://anilist.co/manga/30401"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "the-ghost-in-the-shell-1989",
|
||||||
|
"title": "The Ghost in the Shell",
|
||||||
|
"year": 1989,
|
||||||
|
"period": "1985–1989",
|
||||||
|
"periodOrder": 4,
|
||||||
|
"rank": 4,
|
||||||
|
"note": "Dense cybernetic politics and identity questions; a natural next stop from *BLAME!*.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://kodansha.us/series/the-ghost-in-the-shell/"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx31023-iozeApEMjmPP.jpg",
|
||||||
|
"chapters": 11,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 31023,
|
||||||
|
"url": "https://anilist.co/manga/31023"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "banana-fish-1985",
|
||||||
|
"title": "Banana Fish",
|
||||||
|
"year": 1985,
|
||||||
|
"period": "1985–1989",
|
||||||
|
"periodOrder": 4,
|
||||||
|
"rank": 5,
|
||||||
|
"note": "Crime thriller and devastating relationship drama that crosses demographic boundaries.",
|
||||||
|
"source": {
|
||||||
|
"kind": "read",
|
||||||
|
"url": "https://www.viz.com/vizmanga/chapters/banana-fish"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx30756-rlxjQWBAizHX.jpg",
|
||||||
|
"chapters": 110,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 30756,
|
||||||
|
"url": "https://anilist.co/manga/30756"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ranma-1-2-1987",
|
||||||
|
"title": "Ranma ½",
|
||||||
|
"year": 1987,
|
||||||
|
"period": "1985–1989",
|
||||||
|
"periodOrder": 4,
|
||||||
|
"rank": 6,
|
||||||
|
"note": "Genre-defining gender-bending martial-arts farce with astonishing comic timing.",
|
||||||
|
"source": {
|
||||||
|
"kind": "read",
|
||||||
|
"url": "https://www.viz.com/vizmanga/chapters/ranma"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx30023-NWoD28ZKC73c.jpg",
|
||||||
|
"chapters": 407,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 30023,
|
||||||
|
"url": "https://anilist.co/manga/30023"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "city-hunter-1985",
|
||||||
|
"title": "City Hunter",
|
||||||
|
"year": 1985,
|
||||||
|
"period": "1985–1989",
|
||||||
|
"periodOrder": 4,
|
||||||
|
"rank": 7,
|
||||||
|
"note": "Stylish urban action-comedy with a distinctly late-1980s pulse.",
|
||||||
|
"source": null,
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx30792-62RxEnU7tlDg.png",
|
||||||
|
"chapters": 336,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 30792,
|
||||||
|
"url": "https://anilist.co/manga/30792"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "saint-seiya-1985",
|
||||||
|
"title": "Saint Seiya",
|
||||||
|
"year": 1985,
|
||||||
|
"period": "1985–1989",
|
||||||
|
"periodOrder": 4,
|
||||||
|
"rank": 8,
|
||||||
|
"note": "Mythic armor battles whose melodrama and design traveled worldwide.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://www.viz.com/knights-of-the-zodiac-saint-seiya"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx31045-YUcWBMk7RpeK.png",
|
||||||
|
"chapters": 249,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 31045,
|
||||||
|
"url": "https://anilist.co/manga/31045"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "slam-dunk-1990",
|
||||||
|
"title": "Slam Dunk",
|
||||||
|
"year": 1990,
|
||||||
|
"period": "1990–1994",
|
||||||
|
"periodOrder": 5,
|
||||||
|
"rank": 1,
|
||||||
|
"note": "The sports-manga benchmark: exhilarating games built on genuinely earned character growth.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://www.viz.com/slam-dunk"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx30051-5KJyPlO7z5F4.png",
|
||||||
|
"chapters": 276,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 30051,
|
||||||
|
"url": "https://anilist.co/manga/30051"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "monster-1994",
|
||||||
|
"title": "Monster",
|
||||||
|
"year": 1994,
|
||||||
|
"period": "1990–1994",
|
||||||
|
"periodOrder": 5,
|
||||||
|
"rank": 2,
|
||||||
|
"note": "Urasawa's humane, sprawling suspense novel in manga form; top priority for your mystery shelf.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://www.viz.com/monster"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx30001-Knby7l1jevE7.jpg",
|
||||||
|
"chapters": 162,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 30001,
|
||||||
|
"url": "https://anilist.co/manga/30001"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "sailor-moon-1991",
|
||||||
|
"title": "Sailor Moon",
|
||||||
|
"year": 1991,
|
||||||
|
"period": "1990–1994",
|
||||||
|
"periodOrder": 5,
|
||||||
|
"rank": 3,
|
||||||
|
"note": "Magical-girl team adventure with lasting iconography and romantic urgency.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://kodansha.us/series/pretty-guardian-sailor-moon/"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx30092-tjzz7ymlFfQu.png",
|
||||||
|
"chapters": 61,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 30092,
|
||||||
|
"url": "https://anilist.co/manga/30092"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "yu-yu-hakusho-1990",
|
||||||
|
"title": "Yu Yu Hakusho",
|
||||||
|
"year": 1990,
|
||||||
|
"period": "1990–1994",
|
||||||
|
"periodOrder": 5,
|
||||||
|
"rank": 4,
|
||||||
|
"note": "Supernatural tournament storytelling with unusually lively characters.",
|
||||||
|
"source": {
|
||||||
|
"kind": "read",
|
||||||
|
"url": "https://www.viz.com/shonenjump/chapters/yuyu-hakusho"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx30053-wCR6xyGzeUYo.png",
|
||||||
|
"chapters": 176,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 30053,
|
||||||
|
"url": "https://anilist.co/manga/30053"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "blade-of-the-immortal-1993",
|
||||||
|
"title": "Blade of the Immortal",
|
||||||
|
"year": 1993,
|
||||||
|
"period": "1990–1994",
|
||||||
|
"periodOrder": 5,
|
||||||
|
"rank": 5,
|
||||||
|
"note": "Violent samurai revenge with extraordinary ink work and moral rot.",
|
||||||
|
"source": {
|
||||||
|
"kind": "publisher catalog",
|
||||||
|
"url": "https://images.darkhorse.com/libraries/downloads/LIBCON25_ESSENTIALS.pdf"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/b30658-4KzCEiCDN7Yq.jpg",
|
||||||
|
"chapters": 219,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 30658,
|
||||||
|
"url": "https://anilist.co/manga/30658"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "detective-conan-case-closed-1994",
|
||||||
|
"title": "Detective Conan / Case Closed",
|
||||||
|
"year": 1994,
|
||||||
|
"period": "1990–1994",
|
||||||
|
"periodOrder": 5,
|
||||||
|
"rank": 6,
|
||||||
|
"note": "An immense fair-play mystery machine; best sampled in arcs rather than treated as a completion project.",
|
||||||
|
"source": {
|
||||||
|
"kind": "read",
|
||||||
|
"url": "https://www.viz.com/vizmanga/chapters/case-closed"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx31061-SJQb7931MG7m.jpg",
|
||||||
|
"chapters": null,
|
||||||
|
"status": "ongoing",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 31061,
|
||||||
|
"url": "https://anilist.co/manga/31061"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "basara-1990",
|
||||||
|
"title": "Basara",
|
||||||
|
"year": 1990,
|
||||||
|
"period": "1990–1994",
|
||||||
|
"periodOrder": 5,
|
||||||
|
"rank": 7,
|
||||||
|
"note": "Post-apocalyptic shōjo revolution, romance, and political identity; a great overlooked bridge between genres.",
|
||||||
|
"source": {
|
||||||
|
"kind": "read",
|
||||||
|
"url": "https://www.viz.com/vizmanga/chapters/basara"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx30974-Ck0pncZ7LEnW.png",
|
||||||
|
"chapters": 107,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 30974,
|
||||||
|
"url": "https://anilist.co/manga/30974"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rurouni-kenshin-1994",
|
||||||
|
"title": "Rurouni Kenshin",
|
||||||
|
"year": 1994,
|
||||||
|
"period": "1990–1994",
|
||||||
|
"periodOrder": 5,
|
||||||
|
"rank": 8,
|
||||||
|
"note": "Influential Meiji-era action with a violence-versus-repentance core.",
|
||||||
|
"source": {
|
||||||
|
"kind": "read",
|
||||||
|
"url": "https://mangaplus.shueisha.co.jp/titles/100023"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx30022-jxTlViun1o10.jpg",
|
||||||
|
"chapters": 259,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 30022,
|
||||||
|
"url": "https://anilist.co/manga/30022"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "one-piece-1997",
|
||||||
|
"title": "One Piece",
|
||||||
|
"year": 1997,
|
||||||
|
"period": "1995–1999",
|
||||||
|
"periodOrder": 6,
|
||||||
|
"rank": 1,
|
||||||
|
"note": "Unmatched adventure-world construction and emotional long-game; already in your library.",
|
||||||
|
"source": {
|
||||||
|
"kind": "read",
|
||||||
|
"url": "https://mangaplus.shueisha.co.jp/titles/100020"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx30013-BeslEMqiPhlk.jpg",
|
||||||
|
"chapters": null,
|
||||||
|
"status": "ongoing",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 30013,
|
||||||
|
"url": "https://anilist.co/manga/30013"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "vagabond-1998",
|
||||||
|
"title": "Vagabond",
|
||||||
|
"year": 1998,
|
||||||
|
"period": "1995–1999",
|
||||||
|
"periodOrder": 6,
|
||||||
|
"rank": 2,
|
||||||
|
"note": "Philosophical swordsmanship and breathtaking draftsmanship; strong *Berserk* crossover.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://www.viz.com/vagabond"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx30656-9mW113O7rDnA.png",
|
||||||
|
"chapters": null,
|
||||||
|
"status": "ongoing",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 30656,
|
||||||
|
"url": "https://anilist.co/manga/30656"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "hunter-hunter-1998",
|
||||||
|
"title": "Hunter × Hunter",
|
||||||
|
"year": 1998,
|
||||||
|
"period": "1995–1999",
|
||||||
|
"periodOrder": 6,
|
||||||
|
"rank": 3,
|
||||||
|
"note": "Battle manga as evolving strategy game and moral experiment.",
|
||||||
|
"source": {
|
||||||
|
"kind": "read",
|
||||||
|
"url": "https://mangaplus.shueisha.co.jp/titles/100015"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx30026-uCvXMudMzmwI.jpg",
|
||||||
|
"chapters": null,
|
||||||
|
"status": "ongoing",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 30026,
|
||||||
|
"url": "https://anilist.co/manga/30026"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "20th-century-boys-1999",
|
||||||
|
"title": "20th Century Boys",
|
||||||
|
"year": 1999,
|
||||||
|
"period": "1995–1999",
|
||||||
|
"periodOrder": 6,
|
||||||
|
"rank": 4,
|
||||||
|
"note": "Conspiracy mystery across generations; a near-obvious pick if *Monster* lands.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://www.viz.com/naoki-urasawa-s-20th-century-boys"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx30003-E84fwIh22LAQ.jpg",
|
||||||
|
"chapters": 249,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 30003,
|
||||||
|
"url": "https://anilist.co/manga/30003"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "blame-1997",
|
||||||
|
"title": "BLAME!",
|
||||||
|
"year": 1997,
|
||||||
|
"period": "1995–1999",
|
||||||
|
"periodOrder": 6,
|
||||||
|
"rank": 5,
|
||||||
|
"note": "Architectural terror and almost wordless cyberpunk; present on your shelf.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://kodansha.us/series/blame/"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx30149-YetcNrfacNVj.jpg",
|
||||||
|
"chapters": 66,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 30149,
|
||||||
|
"url": "https://anilist.co/manga/30149"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "uzumaki-1998",
|
||||||
|
"title": "Uzumaki",
|
||||||
|
"year": 1998,
|
||||||
|
"period": "1995–1999",
|
||||||
|
"periodOrder": 6,
|
||||||
|
"rank": 6,
|
||||||
|
"note": "Junji Ito's spiraling small-town cosmic horror, formally as inventive as it is grotesque.",
|
||||||
|
"source": {
|
||||||
|
"kind": "read",
|
||||||
|
"url": "https://www.viz.com/vizmanga/chapters/uzumaki"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx30436-Opk9ZubMPuDU.jpg",
|
||||||
|
"chapters": 20,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 30436,
|
||||||
|
"url": "https://anilist.co/manga/30436"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fruits-basket-1998",
|
||||||
|
"title": "Fruits Basket",
|
||||||
|
"year": 1998,
|
||||||
|
"period": "1995–1999",
|
||||||
|
"periodOrder": 6,
|
||||||
|
"rank": 7,
|
||||||
|
"note": "A shōjo classic that turns supernatural family setup into a long trauma-and-healing story.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://yenpress.com/series/fruits-basket-collector-s-edition"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx30102-MJuQ0e0k2CgU.png",
|
||||||
|
"chapters": 137,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 30102,
|
||||||
|
"url": "https://anilist.co/manga/30102"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "naruto-1999",
|
||||||
|
"title": "Naruto",
|
||||||
|
"year": 1999,
|
||||||
|
"period": "1995–1999",
|
||||||
|
"periodOrder": 6,
|
||||||
|
"rank": 8,
|
||||||
|
"note": "Found-family ninja epic whose strongest arcs show why it became a worldwide entry point.",
|
||||||
|
"source": {
|
||||||
|
"kind": "read",
|
||||||
|
"url": "https://mangaplus.shueisha.co.jp/titles/100018"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/nx30011-9yUF1dXWgDOx.jpg",
|
||||||
|
"chapters": 700,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 30011,
|
||||||
|
"url": "https://anilist.co/manga/30011"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gto-great-teacher-onizuka-1997",
|
||||||
|
"title": "GTO: Great Teacher Onizuka",
|
||||||
|
"year": 1997,
|
||||||
|
"period": "1995–1999",
|
||||||
|
"periodOrder": 6,
|
||||||
|
"rank": 9,
|
||||||
|
"note": "Juvenile comedy undercut by a sincere defense of troubled students.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://kodansha.us/series/gto-great-teacher-onizuka/"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx30336-KWTPMwsNTeSa.png",
|
||||||
|
"chapters": 208,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 30336,
|
||||||
|
"url": "https://anilist.co/manga/30336"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fullmetal-alchemist-2001",
|
||||||
|
"title": "Fullmetal Alchemist",
|
||||||
|
"year": 2001,
|
||||||
|
"period": "2000–2004",
|
||||||
|
"periodOrder": 7,
|
||||||
|
"rank": 1,
|
||||||
|
"note": "Probably the most satisfying closed-loop fantasy adventure of its era: alchemy, war, grief, and consequence.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://www.viz.com/fullmetal-alchemist"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx30025-mpPVpCKFTowt.png",
|
||||||
|
"chapters": 116,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 30025,
|
||||||
|
"url": "https://anilist.co/manga/30025"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "death-note-2003",
|
||||||
|
"title": "Death Note",
|
||||||
|
"year": 2003,
|
||||||
|
"period": "2000–2004",
|
||||||
|
"periodOrder": 7,
|
||||||
|
"rank": 2,
|
||||||
|
"note": "A near-perfect high-concept cat-and-mouse hook, especially in its early stretch.",
|
||||||
|
"source": {
|
||||||
|
"kind": "read",
|
||||||
|
"url": "https://mangaplus.shueisha.co.jp/titles/100008"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx30021-FE6kmrfpuKyb.jpg",
|
||||||
|
"chapters": 108,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 30021,
|
||||||
|
"url": "https://anilist.co/manga/30021"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "nana-2000",
|
||||||
|
"title": "Nana",
|
||||||
|
"year": 2000,
|
||||||
|
"period": "2000–2004",
|
||||||
|
"periodOrder": 7,
|
||||||
|
"rank": 3,
|
||||||
|
"note": "Adult friendship, music, romance, and compromise with a rare emotional bite.",
|
||||||
|
"source": {
|
||||||
|
"kind": "read",
|
||||||
|
"url": "https://www.viz.com/vizmanga/chapters/nana"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx30028-VJqBC1ar6AxE.png",
|
||||||
|
"chapters": null,
|
||||||
|
"status": "ongoing",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 30028,
|
||||||
|
"url": "https://anilist.co/manga/30028"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pluto-2003",
|
||||||
|
"title": "Pluto",
|
||||||
|
"year": 2003,
|
||||||
|
"period": "2000–2004",
|
||||||
|
"periodOrder": 7,
|
||||||
|
"rank": 4,
|
||||||
|
"note": "*Astro Boy* rebuilt as a war-trauma murder mystery by the creator of *Monster*.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://www.viz.com/pluto-urasawa-x-tezuka"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx30745-kueBpwriQkvZ.jpg",
|
||||||
|
"chapters": 65,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 30745,
|
||||||
|
"url": "https://anilist.co/manga/30745"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "yotsuba-2003",
|
||||||
|
"title": "Yotsuba&!",
|
||||||
|
"year": 2003,
|
||||||
|
"period": "2000–2004",
|
||||||
|
"periodOrder": 7,
|
||||||
|
"rank": 5,
|
||||||
|
"note": "Observation and joy made into a near-flawless quiet comedy; a palate cleanser worth the detour.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://yenpress.com/series/yotsuba"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx30104-sUVzNlTWZ5cu.jpg",
|
||||||
|
"chapters": null,
|
||||||
|
"status": "ongoing",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 30104,
|
||||||
|
"url": "https://anilist.co/manga/30104"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "bleach-2001",
|
||||||
|
"title": "Bleach",
|
||||||
|
"year": 2001,
|
||||||
|
"period": "2000–2004",
|
||||||
|
"periodOrder": 7,
|
||||||
|
"rank": 6,
|
||||||
|
"note": "Aesthetic, sword-fighting, and character-design powerhouse already on your shelf.",
|
||||||
|
"source": {
|
||||||
|
"kind": "read",
|
||||||
|
"url": "https://mangaplus.shueisha.co.jp/titles/100004"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx30012-1epmVfTSv2rr.png",
|
||||||
|
"chapters": 706,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 30012,
|
||||||
|
"url": "https://anilist.co/manga/30012"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gantz-2000",
|
||||||
|
"title": "Gantz",
|
||||||
|
"year": 2000,
|
||||||
|
"period": "2000–2004",
|
||||||
|
"periodOrder": 7,
|
||||||
|
"rank": 7,
|
||||||
|
"note": "Messy, excessive survival SF with page-turning momentum; one of your more direct taste matches.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://www.darkhorse.com/Books/30-524/Gantz-Omnibus-Volume-1-TPB"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx30564-RbyFbLzbnKea.jpg",
|
||||||
|
"chapters": 383,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 30564,
|
||||||
|
"url": "https://anilist.co/manga/30564"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "nodame-cantabile-2001",
|
||||||
|
"title": "Nodame Cantabile",
|
||||||
|
"year": 2001,
|
||||||
|
"period": "2000–2004",
|
||||||
|
"periodOrder": 7,
|
||||||
|
"rank": 8,
|
||||||
|
"note": "Music-school romantic comedy that actually cares about craft and artistic ambition.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://kodansha.us/series/nodame-cantabile/"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx30419-lT7p72ItXPNK.jpg",
|
||||||
|
"chapters": 152,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 30419,
|
||||||
|
"url": "https://anilist.co/manga/30419"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "vinland-saga-2005",
|
||||||
|
"title": "Vinland Saga",
|
||||||
|
"year": 2005,
|
||||||
|
"period": "2005–2009",
|
||||||
|
"periodOrder": 8,
|
||||||
|
"rank": 1,
|
||||||
|
"note": "Viking violence transformed into a remarkable argument about what strength is for.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://kodansha.us/series/vinland-saga/"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx30642-0mjRDkf4THpo.jpg",
|
||||||
|
"chapters": 224,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 30642,
|
||||||
|
"url": "https://anilist.co/manga/30642"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "kingdom-2006",
|
||||||
|
"title": "Kingdom",
|
||||||
|
"year": 2006,
|
||||||
|
"period": "2005–2009",
|
||||||
|
"periodOrder": 8,
|
||||||
|
"rank": 2,
|
||||||
|
"note": "Huge-scale Chinese-war strategy and addictive career progression; likely strong for your *Solo Leveling*/*One Piece* side.",
|
||||||
|
"source": {
|
||||||
|
"kind": "read",
|
||||||
|
"url": "https://www.viz.com/shonenjump/chapters/kingdom"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/nx46765-KPXir4sRqJBW.png",
|
||||||
|
"chapters": null,
|
||||||
|
"status": "ongoing",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 46765,
|
||||||
|
"url": "https://anilist.co/manga/46765"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "goodnight-punpun-2007",
|
||||||
|
"title": "Goodnight Punpun",
|
||||||
|
"year": 2007,
|
||||||
|
"period": "2005–2009",
|
||||||
|
"periodOrder": 8,
|
||||||
|
"rank": 3,
|
||||||
|
"note": "Formal daring and psychologically brutal coming-of-age; brilliant, but heed the heavy-content warning.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://www.viz.com/goodnight-punpun"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx34632-5xMDkx3pXsEh.png",
|
||||||
|
"chapters": 147,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 34632,
|
||||||
|
"url": "https://anilist.co/manga/34632"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "chihayafuru-2007",
|
||||||
|
"title": "Chihayafuru",
|
||||||
|
"year": 2007,
|
||||||
|
"period": "2005–2009",
|
||||||
|
"periodOrder": 8,
|
||||||
|
"rank": 4,
|
||||||
|
"note": "Makes competitive karuta feel as tense as any battle manga, without losing its relationships.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://kodansha.us/series/chihayafuru/"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/nx43245-Ied8R99LH3u6.jpg",
|
||||||
|
"chapters": 248,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 43245,
|
||||||
|
"url": "https://anilist.co/manga/43245"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "a-bride-s-story-2008",
|
||||||
|
"title": "A Bride's Story",
|
||||||
|
"year": 2008,
|
||||||
|
"period": "2005–2009",
|
||||||
|
"periodOrder": 8,
|
||||||
|
"rank": 5,
|
||||||
|
"note": "Astonishingly detailed Central Asian historical slice of life and household drama.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://yenpress.com/series/a-bride-s-story"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx41514-IHeXFwqmWivk.jpg",
|
||||||
|
"chapters": null,
|
||||||
|
"status": "ongoing",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 41514,
|
||||||
|
"url": "https://anilist.co/manga/41514"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "attack-on-titan-2009",
|
||||||
|
"title": "Attack on Titan",
|
||||||
|
"year": 2009,
|
||||||
|
"period": "2005–2009",
|
||||||
|
"periodOrder": 8,
|
||||||
|
"rank": 6,
|
||||||
|
"note": "Escalating siege mystery that repeatedly changes the reader's understanding of its world.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://kodansha.us/series/attack-on-titan/"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx53390-1RsuABC34P9D.jpg",
|
||||||
|
"chapters": 141,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 53390,
|
||||||
|
"url": "https://anilist.co/manga/53390"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "march-comes-in-like-a-lion-2007",
|
||||||
|
"title": "March Comes in Like a Lion",
|
||||||
|
"year": 2007,
|
||||||
|
"period": "2005–2009",
|
||||||
|
"periodOrder": 8,
|
||||||
|
"rank": 7,
|
||||||
|
"note": "Shogi, loneliness, and hard-won human warmth; a quieter prestige pick.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://www.denpa.pub/book"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx31224-dxugU08jahRT.jpg",
|
||||||
|
"chapters": null,
|
||||||
|
"status": "ongoing",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 31224,
|
||||||
|
"url": "https://anilist.co/manga/31224"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "bakuman-2008",
|
||||||
|
"title": "Bakuman",
|
||||||
|
"year": 2008,
|
||||||
|
"period": "2005–2009",
|
||||||
|
"periodOrder": 8,
|
||||||
|
"rank": 8,
|
||||||
|
"note": "An entertaining and revealing, if idealized, story about making a hit manga.",
|
||||||
|
"source": {
|
||||||
|
"kind": "read",
|
||||||
|
"url": "https://mangaplus.shueisha.co.jp/titles/100002"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx39711-tjPWXT1AW321.jpg",
|
||||||
|
"chapters": 176,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 39711,
|
||||||
|
"url": "https://anilist.co/manga/39711"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "golden-kamuy-2014",
|
||||||
|
"title": "Golden Kamuy",
|
||||||
|
"year": 2014,
|
||||||
|
"period": "2010–2014",
|
||||||
|
"periodOrder": 9,
|
||||||
|
"rank": 1,
|
||||||
|
"note": "Ainu history, treasure hunt, survival cuisine, grotesque comedy: gloriously hard to categorize.",
|
||||||
|
"source": {
|
||||||
|
"kind": "read",
|
||||||
|
"url": "https://www.viz.com/shonenjump/chapters/golden-kamuy"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx86559-Y4xDmoz4ud43.jpg",
|
||||||
|
"chapters": 314,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 86559,
|
||||||
|
"url": "https://anilist.co/manga/86559"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "land-of-the-lustrous-2012",
|
||||||
|
"title": "Land of the Lustrous",
|
||||||
|
"year": 2012,
|
||||||
|
"period": "2010–2014",
|
||||||
|
"periodOrder": 9,
|
||||||
|
"rank": 2,
|
||||||
|
"note": "Immortal gemstone bodies and slowly devastating identity change; exquisite speculative manga.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://kodansha.us/series/land-of-the-lustrous/"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx74489-5HtDCFfut8Be.jpg",
|
||||||
|
"chapters": 120,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 74489,
|
||||||
|
"url": "https://anilist.co/manga/74489"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "delicious-in-dungeon-2014",
|
||||||
|
"title": "Delicious in Dungeon",
|
||||||
|
"year": 2014,
|
||||||
|
"period": "2010–2014",
|
||||||
|
"periodOrder": 9,
|
||||||
|
"rank": 3,
|
||||||
|
"note": "Dungeon ecology and cooking become a serious fantasy-world argument.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://yenpress.com/series/delicious-in-dungeon"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx86082-MXizJxzbijdd.jpg",
|
||||||
|
"chapters": 111,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 86082,
|
||||||
|
"url": "https://anilist.co/manga/86082"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "one-punch-man-2012",
|
||||||
|
"title": "One-Punch Man",
|
||||||
|
"year": 2012,
|
||||||
|
"period": "2010–2014",
|
||||||
|
"periodOrder": 9,
|
||||||
|
"rank": 4,
|
||||||
|
"note": "Superhero parody with implausibly spectacular fight art; present locally.",
|
||||||
|
"source": {
|
||||||
|
"kind": "read",
|
||||||
|
"url": "https://www.viz.com/shonenjump/chapters/one-punch-man"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx74347-sZpmNJ5xLwRK.jpg",
|
||||||
|
"chapters": null,
|
||||||
|
"status": "ongoing",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 74347,
|
||||||
|
"url": "https://anilist.co/manga/74347"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "haikyu-2012",
|
||||||
|
"title": "Haikyu!!",
|
||||||
|
"year": 2012,
|
||||||
|
"period": "2010–2014",
|
||||||
|
"periodOrder": 9,
|
||||||
|
"rank": 5,
|
||||||
|
"note": "Modern sports manga's great ensemble and a model of readable action.",
|
||||||
|
"source": {
|
||||||
|
"kind": "read",
|
||||||
|
"url": "https://mangaplus.shueisha.co.jp/titles/100014"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx65243-mR4MnJFmfaOF.png",
|
||||||
|
"chapters": 404,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 65243,
|
||||||
|
"url": "https://anilist.co/manga/65243"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "a-silent-voice-2013",
|
||||||
|
"title": "A Silent Voice",
|
||||||
|
"year": 2013,
|
||||||
|
"period": "2010–2014",
|
||||||
|
"periodOrder": 9,
|
||||||
|
"rank": 6,
|
||||||
|
"note": "Accountability and disability treated with an unusually painful, compassionate eye.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://kodansha.us/series/a-silent-voice/"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx85135-11OOnyaqV71k.png",
|
||||||
|
"chapters": 64,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 85135,
|
||||||
|
"url": "https://anilist.co/manga/85135"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ajin-demi-human-2012",
|
||||||
|
"title": "Ajin: Demi-Human",
|
||||||
|
"year": 2012,
|
||||||
|
"period": "2010–2014",
|
||||||
|
"periodOrder": 9,
|
||||||
|
"rank": 7,
|
||||||
|
"note": "Tactical immortal-on-immortal thriller; a particularly strong fit for your action/SF preferences.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://kodansha.us/series/ajin-demi-human/"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/b79865-rwKmISrhJJyP.jpg",
|
||||||
|
"chapters": 99,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 79865,
|
||||||
|
"url": "https://anilist.co/manga/79865"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "blue-giant-2013",
|
||||||
|
"title": "Blue Giant",
|
||||||
|
"year": 2013,
|
||||||
|
"period": "2010–2014",
|
||||||
|
"periodOrder": 9,
|
||||||
|
"rank": 8,
|
||||||
|
"note": "A jazz practice-and-ambition manga that somehow makes performance visibly loud.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://sevenseasentertainment.com/series/blue-giant-omnibus/"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx86890-hYxIiVM6rBBG.jpg",
|
||||||
|
"chapters": 80,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 86890,
|
||||||
|
"url": "https://anilist.co/manga/86890"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "witch-hat-atelier-2016",
|
||||||
|
"title": "Witch Hat Atelier",
|
||||||
|
"year": 2016,
|
||||||
|
"period": "2015–2019",
|
||||||
|
"periodOrder": 10,
|
||||||
|
"rank": 1,
|
||||||
|
"note": "Beautifully designed magic with politics, disability, and apprenticeship underneath; 2025 Harvey Best Manga winner.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://kodansha.us/series/witch-hat-atelier/"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx98263-kIeaTXrAbGkj.jpg",
|
||||||
|
"chapters": null,
|
||||||
|
"status": "ongoing",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 98263,
|
||||||
|
"url": "https://anilist.co/manga/98263"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "chainsaw-man-2018",
|
||||||
|
"title": "Chainsaw Man",
|
||||||
|
"year": 2018,
|
||||||
|
"period": "2015–2019",
|
||||||
|
"periodOrder": 10,
|
||||||
|
"rank": 2,
|
||||||
|
"note": "Chaotic horror-action that keeps finding a more human emotional center.",
|
||||||
|
"source": {
|
||||||
|
"kind": "read",
|
||||||
|
"url": "https://mangaplus.shueisha.co.jp/titles/100037"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx105778-euxXZEIfDY2u.png",
|
||||||
|
"chapters": 232,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 105778,
|
||||||
|
"url": "https://anilist.co/manga/105778"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "blue-period-2017",
|
||||||
|
"title": "Blue Period",
|
||||||
|
"year": 2017,
|
||||||
|
"period": "2015–2019",
|
||||||
|
"periodOrder": 10,
|
||||||
|
"rank": 3,
|
||||||
|
"note": "Art-making as vocation, insecurity, and strenuous learning; a Kodansha Manga Award winner.",
|
||||||
|
"source": {
|
||||||
|
"kind": "award history",
|
||||||
|
"url": "https://www.kodansha.co.jp/awards/comics/histories"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx107237-tyQXghsKmE0t.jpg",
|
||||||
|
"chapters": null,
|
||||||
|
"status": "ongoing",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 107237,
|
||||||
|
"url": "https://anilist.co/manga/107237"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "beastars-2016",
|
||||||
|
"title": "Beastars",
|
||||||
|
"year": 2016,
|
||||||
|
"period": "2015–2019",
|
||||||
|
"periodOrder": 10,
|
||||||
|
"rank": 4,
|
||||||
|
"note": "Predator/prey noir and social allegory with an odd, tender character core; award-recognized.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://www.viz.com/beastars"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx98587-useiMvZ0nfya.png",
|
||||||
|
"chapters": 196,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 98587,
|
||||||
|
"url": "https://anilist.co/manga/98587"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "the-apothecary-diaries-2017",
|
||||||
|
"title": "The Apothecary Diaries",
|
||||||
|
"year": 2017,
|
||||||
|
"period": "2015–2019",
|
||||||
|
"periodOrder": 10,
|
||||||
|
"rank": 5,
|
||||||
|
"note": "Court intrigue solved by a poison-obsessed medical detective.",
|
||||||
|
"source": {
|
||||||
|
"kind": "read",
|
||||||
|
"url": "https://global.manga-up.com/manga/164"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/nx99022-Hh2WdyNgR8HM.jpg",
|
||||||
|
"chapters": null,
|
||||||
|
"status": "ongoing",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 99022,
|
||||||
|
"url": "https://anilist.co/manga/99022"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "heavenly-delusion-2018",
|
||||||
|
"title": "Heavenly Delusion",
|
||||||
|
"year": 2018,
|
||||||
|
"period": "2015–2019",
|
||||||
|
"periodOrder": 10,
|
||||||
|
"rank": 6,
|
||||||
|
"note": "Two intertwined post-apocalyptic mysteries; a highly specific match to your library.",
|
||||||
|
"source": {
|
||||||
|
"kind": "read",
|
||||||
|
"url": "https://kmanga.kodansha.com/title/10511/episode/345745"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx101942-yjslDpT6XcpK.jpg",
|
||||||
|
"chapters": null,
|
||||||
|
"status": "ongoing",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 101942,
|
||||||
|
"url": "https://anilist.co/manga/101942"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "demon-slayer-kimetsu-no-yaiba-2016",
|
||||||
|
"title": "Demon Slayer: Kimetsu no Yaiba",
|
||||||
|
"year": 2016,
|
||||||
|
"period": "2015–2019",
|
||||||
|
"periodOrder": 10,
|
||||||
|
"rank": 7,
|
||||||
|
"note": "Cleanly motivated tragedy and unusually readable, concentrated action.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://www.viz.com/demon-slayer-kimetsu-no-yaiba"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx87216-c9bSNVD10UuD.png",
|
||||||
|
"chapters": 207,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 87216,
|
||||||
|
"url": "https://anilist.co/manga/87216"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "jujutsu-kaisen-2018",
|
||||||
|
"title": "Jujutsu Kaisen",
|
||||||
|
"year": 2018,
|
||||||
|
"period": "2015–2019",
|
||||||
|
"periodOrder": 10,
|
||||||
|
"rank": 8,
|
||||||
|
"note": "Dark occult combat with inventive constraints and breakneck tactical reversals.",
|
||||||
|
"source": {
|
||||||
|
"kind": "read",
|
||||||
|
"url": "https://mangaplus.shueisha.co.jp/titles/100034"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx101517-H3TdM3g5ZUe9.jpg",
|
||||||
|
"chapters": 272,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 101517,
|
||||||
|
"url": "https://anilist.co/manga/101517"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "spy-family-2019",
|
||||||
|
"title": "Spy × Family",
|
||||||
|
"year": 2019,
|
||||||
|
"period": "2015–2019",
|
||||||
|
"periodOrder": 10,
|
||||||
|
"rank": 9,
|
||||||
|
"note": "Found-family spy comedy that uses its gimmick for real tenderness.",
|
||||||
|
"source": {
|
||||||
|
"kind": "read",
|
||||||
|
"url": "https://mangaplus.shueisha.co.jp/titles/100056"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx108556-NHjkz0BNJhLx.jpg",
|
||||||
|
"chapters": null,
|
||||||
|
"status": "ongoing",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 108556,
|
||||||
|
"url": "https://anilist.co/manga/108556"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "kaguya-sama-love-is-war-2015",
|
||||||
|
"title": "Kaguya-sama: Love Is War",
|
||||||
|
"year": 2015,
|
||||||
|
"period": "2015–2019",
|
||||||
|
"periodOrder": 10,
|
||||||
|
"rank": 10,
|
||||||
|
"note": "Romantic comedy as elaborate psychological warfare, then sincere character study.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://www.viz.com/kaguya-sama-love-is-war"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx86635-EdaLQmsn86Fy.png",
|
||||||
|
"chapters": 281,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 86635,
|
||||||
|
"url": "https://anilist.co/manga/86635"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "frieren-beyond-journey-s-end-2020",
|
||||||
|
"title": "Frieren: Beyond Journey's End",
|
||||||
|
"year": 2020,
|
||||||
|
"period": "2020–2024",
|
||||||
|
"periodOrder": 11,
|
||||||
|
"rank": 1,
|
||||||
|
"note": "Post-quest fantasy about the scale of memory and grief; 2021 Manga Taisho winner and a strong fit for your fantasy shelf.",
|
||||||
|
"source": {
|
||||||
|
"kind": "read",
|
||||||
|
"url": "https://www.viz.com/vizmanga/chapters/frieren-the-journeys-end"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx118586-CXKgWikBFQgS.jpg",
|
||||||
|
"chapters": null,
|
||||||
|
"status": "ongoing",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 118586,
|
||||||
|
"url": "https://anilist.co/manga/118586"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "dandadan-2021",
|
||||||
|
"title": "Dandadan",
|
||||||
|
"year": 2021,
|
||||||
|
"period": "2020–2024",
|
||||||
|
"periodOrder": 11,
|
||||||
|
"rank": 2,
|
||||||
|
"note": "UFOs, ghosts, young romance, and some of the most kinetic pages in weekly manga.",
|
||||||
|
"source": {
|
||||||
|
"kind": "read",
|
||||||
|
"url": "https://mangaplus.shueisha.co.jp/titles/100171"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx132029-prGF4gePdSKv.jpg",
|
||||||
|
"chapters": null,
|
||||||
|
"status": "ongoing",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 132029,
|
||||||
|
"url": "https://anilist.co/manga/132029"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "the-summer-hikaru-died-2021",
|
||||||
|
"title": "The Summer Hikaru Died",
|
||||||
|
"year": 2021,
|
||||||
|
"period": "2020–2024",
|
||||||
|
"periodOrder": 11,
|
||||||
|
"rank": 3,
|
||||||
|
"note": "Queer grief and rural body horror with extraordinary unease.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://yenpress.com/series/the-summer-hikaru-died"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx138603-j0gcshf7PVkN.jpg",
|
||||||
|
"chapters": null,
|
||||||
|
"status": "ongoing",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 138603,
|
||||||
|
"url": "https://anilist.co/manga/138603"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "medalist-2020",
|
||||||
|
"title": "Medalist",
|
||||||
|
"year": 2020,
|
||||||
|
"period": "2020–2024",
|
||||||
|
"periodOrder": 11,
|
||||||
|
"rank": 4,
|
||||||
|
"note": "Figure-skating mentorship with rigorous athletic detail and big emotional payoffs.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://kodansha.us/series/medalist/"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx118371-3xk9VUbWnnRR.jpg",
|
||||||
|
"chapters": null,
|
||||||
|
"status": "ongoing",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 118371,
|
||||||
|
"url": "https://anilist.co/manga/118371"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "orb-on-the-movements-of-the-earth-2020",
|
||||||
|
"title": "Orb: On the Movements of the Earth",
|
||||||
|
"year": 2020,
|
||||||
|
"period": "2020–2024",
|
||||||
|
"periodOrder": 11,
|
||||||
|
"rank": 5,
|
||||||
|
"note": "A dangerous idea passed between people across generations; philosophical historical thriller.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://sevenseasentertainment.com/series/orb-on-the-movements-of-the-earth-omnibus/"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx124139-OOL1bW0HecJT.jpg",
|
||||||
|
"chapters": 62,
|
||||||
|
"status": "completed",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 124139,
|
||||||
|
"url": "https://anilist.co/manga/124139"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "the-darwin-incident-2020",
|
||||||
|
"title": "The Darwin Incident",
|
||||||
|
"year": 2020,
|
||||||
|
"period": "2020–2024",
|
||||||
|
"periodOrder": 11,
|
||||||
|
"rank": 6,
|
||||||
|
"note": "A human–chimpanzee teenager turns a political-thriller premise into questions of personhood; 2026 Kodansha Manga Award winner.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://kodansha.us/series/the-darwin-incident/"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx120638-cWwWsAGL3t9d.jpg",
|
||||||
|
"chapters": null,
|
||||||
|
"status": "ongoing",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 120638,
|
||||||
|
"url": "https://anilist.co/manga/120638"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "kagurabachi-2023",
|
||||||
|
"title": "Kagurabachi",
|
||||||
|
"year": 2023,
|
||||||
|
"period": "2020–2024",
|
||||||
|
"periodOrder": 11,
|
||||||
|
"rank": 7,
|
||||||
|
"note": "Sword-revenge action that rapidly grew beyond its meme beginnings into a genuinely stylish hit.",
|
||||||
|
"source": {
|
||||||
|
"kind": "read",
|
||||||
|
"url": "https://mangaplus.shueisha.co.jp/titles/100274"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx169355-5pqzh1Wb4NOQ.png",
|
||||||
|
"chapters": null,
|
||||||
|
"status": "ongoing",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 169355,
|
||||||
|
"url": "https://anilist.co/manga/169355"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gachiakuta-2022",
|
||||||
|
"title": "Gachiakuta",
|
||||||
|
"year": 2022,
|
||||||
|
"period": "2020–2024",
|
||||||
|
"periodOrder": 11,
|
||||||
|
"rank": 8,
|
||||||
|
"note": "Graffiti-inflected junkyard fantasy and kinetic social anger; 2026 Kodansha Manga Award winner.",
|
||||||
|
"source": {
|
||||||
|
"kind": "edition",
|
||||||
|
"url": "https://kodansha.us/series/gachiakuta/"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx144946-cscic3n2SwdY.jpg",
|
||||||
|
"chapters": null,
|
||||||
|
"status": "ongoing",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 144946,
|
||||||
|
"url": "https://anilist.co/manga/144946"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "sakamoto-days-2020",
|
||||||
|
"title": "Sakamoto Days",
|
||||||
|
"year": 2020,
|
||||||
|
"period": "2020–2024",
|
||||||
|
"periodOrder": 11,
|
||||||
|
"rank": 9,
|
||||||
|
"note": "Retired-hitman comedy with action choreography that repeatedly tops its own gimmick.",
|
||||||
|
"source": {
|
||||||
|
"kind": "read",
|
||||||
|
"url": "https://mangaplus.shueisha.co.jp/titles/100127"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx125828-p78Z8SflkfmO.jpg",
|
||||||
|
"chapters": null,
|
||||||
|
"status": "ongoing",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 125828,
|
||||||
|
"url": "https://anilist.co/manga/125828"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ichi-the-witch-2024",
|
||||||
|
"title": "Ichi the Witch",
|
||||||
|
"year": 2024,
|
||||||
|
"period": "2020–2024",
|
||||||
|
"periodOrder": 11,
|
||||||
|
"rank": 10,
|
||||||
|
"note": "A newer spell-hunting fantasy with a charismatic oddball lead; promising but less historically settled than the first six here.",
|
||||||
|
"source": {
|
||||||
|
"kind": "read",
|
||||||
|
"url": "https://www.viz.com/shonenjump/chapters/ichi-the-witch"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx180752-13CVO310XiEs.jpg",
|
||||||
|
"chapters": null,
|
||||||
|
"status": "ongoing",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 180752,
|
||||||
|
"url": "https://anilist.co/manga/180752"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "home-at-the-horizon-2025",
|
||||||
|
"title": "Home at the Horizon",
|
||||||
|
"year": 2025,
|
||||||
|
"period": "2025–2026",
|
||||||
|
"periodOrder": 12,
|
||||||
|
"rank": 1,
|
||||||
|
"note": "Seaside bathhouse family drama; #1 in the 2026 Next Manga Awards web vote.",
|
||||||
|
"source": {
|
||||||
|
"kind": "official award and reading lead",
|
||||||
|
"url": "https://mangaplus.shueisha.co.jp/web_pages/2441/"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx188733-YMOdYQ8llVnm.jpg",
|
||||||
|
"chapters": null,
|
||||||
|
"status": "ongoing",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 188733,
|
||||||
|
"url": "https://anilist.co/manga/188733"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "someone-hertz-2025",
|
||||||
|
"title": "Someone Hertz",
|
||||||
|
"year": 2025,
|
||||||
|
"period": "2025–2026",
|
||||||
|
"periodOrder": 12,
|
||||||
|
"rank": 2,
|
||||||
|
"note": "Comedy/radio romance; #1 in the 2026 print-category reader vote and officially serialized in English.",
|
||||||
|
"source": {
|
||||||
|
"kind": "read",
|
||||||
|
"url": "https://www.viz.com/shonenjump/chapters/someone-hertz"
|
||||||
|
},
|
||||||
|
"cover": null,
|
||||||
|
"chapters": null,
|
||||||
|
"status": "unknown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "futari-bus-2025",
|
||||||
|
"title": "Futari Bus",
|
||||||
|
"year": 2025,
|
||||||
|
"period": "2025–2026",
|
||||||
|
"periodOrder": 12,
|
||||||
|
"rank": 3,
|
||||||
|
"note": "A bus-ride, almost-romance comedy; #2 in the same reader vote.;",
|
||||||
|
"source": {
|
||||||
|
"kind": "award/info",
|
||||||
|
"url": "https://tsugimanga.jp/winners_announcement/comics"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx201855-7dEgpKSZhWsH.jpg",
|
||||||
|
"chapters": null,
|
||||||
|
"status": "ongoing",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 201855,
|
||||||
|
"url": "https://anilist.co/manga/201855"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "war-of-the-adults-2025",
|
||||||
|
"title": "War of the Adults",
|
||||||
|
"year": 2025,
|
||||||
|
"period": "2025–2026",
|
||||||
|
"periodOrder": 12,
|
||||||
|
"rank": 4,
|
||||||
|
"note": "Kappy/Masaaki Tsuzuki's social-conflict thriller; #4 in the 2026 web vote and a better match to your darker shelf.",
|
||||||
|
"source": {
|
||||||
|
"kind": "official award and reading lead",
|
||||||
|
"url": "https://mangaplus.shueisha.co.jp/web_pages/2441/"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx189691-TYkHsBOxbQW8.jpg",
|
||||||
|
"chapters": null,
|
||||||
|
"status": "ongoing",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 189691,
|
||||||
|
"url": "https://anilist.co/manga/189691"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "the-marshal-king-2025",
|
||||||
|
"title": "The Marshal King",
|
||||||
|
"year": 2025,
|
||||||
|
"period": "2025–2026",
|
||||||
|
"periodOrder": 12,
|
||||||
|
"rank": 5,
|
||||||
|
"note": "Boichi's Western-inflected action; one of MANGA Plus's most-viewed 2025 debut chapters, but too young for a legacy claim.",
|
||||||
|
"source": {
|
||||||
|
"kind": "read",
|
||||||
|
"url": "https://www.viz.com/shonenjump/chapters/the-marshal-king"
|
||||||
|
},
|
||||||
|
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx186517-AP09vY1BVb1E.jpg",
|
||||||
|
"chapters": null,
|
||||||
|
"status": "ongoing",
|
||||||
|
"metadata": {
|
||||||
|
"provider": "AniList",
|
||||||
|
"id": 186517,
|
||||||
|
"url": "https://anilist.co/manga/186517"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "hitoner-2026",
|
||||||
|
"title": "Hitoner",
|
||||||
|
"year": 2026,
|
||||||
|
"period": "2025–2026",
|
||||||
|
"periodOrder": 12,
|
||||||
|
"rank": 6,
|
||||||
|
"note": "Human/other-species observation story; #2 in the 2026 web vote. Include as an adventurous wildcard, not a proven classic.",
|
||||||
|
"source": {
|
||||||
|
"kind": "official award and reading lead",
|
||||||
|
"url": "https://mangaplus.shueisha.co.jp/web_pages/2441/"
|
||||||
|
},
|
||||||
|
"cover": null,
|
||||||
|
"chapters": null,
|
||||||
|
"status": "unknown"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
'use strict';
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const RATINGS = Object.freeze(['flame', 'good', 'neutral', 'skip']);
|
||||||
|
const WEIGHT = Object.freeze({ flame: 3, good: 1, neutral: 0, skip: -2 });
|
||||||
|
const normalize = value => String(value || '').normalize('NFKD').toLowerCase().replace(/[^a-z0-9]+/g, '');
|
||||||
|
|
||||||
|
function loadCatalog(file = path.join(__dirname, 'catalog.json')) {
|
||||||
|
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||||
|
if (parsed.schema !== 1 || !Array.isArray(parsed.entries)) throw new Error('Invalid Greatest of All Time catalog');
|
||||||
|
const ids = new Set();
|
||||||
|
for (const entry of parsed.entries) {
|
||||||
|
if (!/^[a-z0-9-]+$/.test(entry.id) || ids.has(entry.id) || !entry.title || !entry.period || !Number.isInteger(entry.rank))
|
||||||
|
throw new Error(`Invalid or duplicate Greatest of All Time entry: ${entry.id}`);
|
||||||
|
if (entry.cover && !/^https:\/\//.test(entry.cover)) throw new Error(`Invalid cover URL for ${entry.id}`);
|
||||||
|
if (entry.source?.url && !/^https:\/\//.test(entry.source.url)) throw new Error(`Invalid source URL for ${entry.id}`);
|
||||||
|
ids.add(entry.id);
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findOwned(entry, mangas) {
|
||||||
|
const names = new Set([entry.title, ...(entry.aliases || [])].map(normalize));
|
||||||
|
return mangas.find(m => {
|
||||||
|
const alt = m.meta?.altTitles;
|
||||||
|
return [m.title, ...(Array.isArray(alt) ? alt : alt ? [alt] : [])].some(name => names.has(normalize(name)));
|
||||||
|
}) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function countsFor(entryId, votes, users) {
|
||||||
|
const counts = { flame: 0, good: 0, neutral: 0, skip: 0 };
|
||||||
|
for (const name of Object.keys(users)) {
|
||||||
|
const rating = votes[name]?.[entryId];
|
||||||
|
if (RATINGS.includes(rating)) counts[rating]++;
|
||||||
|
}
|
||||||
|
return counts;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scoreFor(counts) {
|
||||||
|
return RATINGS.reduce((sum, rating) => sum + counts[rating] * WEIGHT[rating], 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { RATINGS, WEIGHT, normalize, loadCatalog, findOwned, countsFor, scoreFor };
|
||||||
@@ -34,6 +34,7 @@ label { display: block; }
|
|||||||
padding: 8px 16px; padding-top: max(8px, env(safe-area-inset-top)); background: rgba(18, 18, 20, .94); border-bottom: 1px solid var(--line);
|
padding: 8px 16px; padding-top: max(8px, env(safe-area-inset-top)); background: rgba(18, 18, 20, .94); border-bottom: 1px solid var(--line);
|
||||||
}
|
}
|
||||||
.brand { font-weight: 800; text-decoration: none; font-size: 17px; letter-spacing: .2px; }
|
.brand { font-weight: 800; text-decoration: none; font-size: 17px; letter-spacing: .2px; }
|
||||||
|
.nav-greatest { border-color: #5b3c46; color: #f0b9a0; }
|
||||||
.bell-wrap { position: relative; }
|
.bell-wrap { position: relative; }
|
||||||
.bell .count { background: var(--accent); color: #fff; border-radius: 9px; font-size: 12px; font-weight: 700; padding: 0 6px; }
|
.bell .count { background: var(--accent); color: #fff; border-radius: 9px; font-size: 12px; font-weight: 700; padding: 0 6px; }
|
||||||
.panel {
|
.panel {
|
||||||
@@ -100,6 +101,11 @@ main { max-width: 1200px; margin: 0 auto; padding: 16px; }
|
|||||||
.toolbar select { flex: 1; }
|
.toolbar select { flex: 1; }
|
||||||
.grid { grid-template-columns: repeat(2, 1fr); gap: 16px 12px; }
|
.grid { grid-template-columns: repeat(2, 1fr); gap: 16px 12px; }
|
||||||
.nav { padding-left: 12px; padding-right: 12px; }
|
.nav { padding-left: 12px; padding-right: 12px; }
|
||||||
|
.nav { gap: 5px; }
|
||||||
|
.nav .brand { font-size: 14px; }
|
||||||
|
.nav .btn { padding-left: 7px; padding-right: 7px; font-size: 12px; }
|
||||||
|
.nav-greatest { font-size: 0 !important; }
|
||||||
|
.nav-greatest::after { content: 'GOAT'; font-size: 12px; }
|
||||||
.reading-summary { grid-template-columns: 1fr; gap: 5px; }
|
.reading-summary { grid-template-columns: 1fr; gap: 5px; }
|
||||||
.reading-summary-links { justify-content: flex-start; }
|
.reading-summary-links { justify-content: flex-start; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ function topNav(me) {
|
|||||||
document.addEventListener('click', () => { panel.hidden = true; });
|
document.addEventListener('click', () => { panel.hidden = true; });
|
||||||
const nav = h('header', { class: 'nav' },
|
const nav = h('header', { class: 'nav' },
|
||||||
h('a', { class: 'brand', href: './' }, 'MangaCircus'),
|
h('a', { class: 'brand', href: './' }, 'MangaCircus'),
|
||||||
|
h('a', { class: 'btn nav-greatest', href: 'greatest.html', title: 'Shijō Saikō no Manga · Greatest of All Time' }, 'Greatest'),
|
||||||
h('div', { class: 'grow' }),
|
h('div', { class: 'grow' }),
|
||||||
h('div', { class: 'bell-wrap' }, bell, panel),
|
h('div', { class: 'bell-wrap' }, bell, panel),
|
||||||
h('a', { class: 'btn', href: 'settings.html' }, 'Settings'),
|
h('a', { class: 'btn', href: 'settings.html' }, 'Settings'),
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
.greatest-intro { padding: 18px 0 14px; max-width: 750px; }
|
||||||
|
.greatest-intro h1 { font-size: clamp(28px, 5vw, 42px); letter-spacing: -.03em; }
|
||||||
|
.greatest-intro p { margin: 7px 0; }
|
||||||
|
.greatest-eyebrow { color: #e9a47d; text-transform: uppercase; letter-spacing: .15em; font-size: 12px; font-weight: 800; }
|
||||||
|
.greatest-toolbar { display: flex; align-items: center; gap: 10px; margin: 12px 0 10px; }
|
||||||
|
.greatest-toolbar > input { flex: 1; min-width: 0; }
|
||||||
|
.greatest-sort, .greatest-filters { display: flex; flex-wrap: wrap; gap: 7px; }
|
||||||
|
.greatest-filters { margin: 10px 0 12px; }
|
||||||
|
.greatest-periods { margin-top: 12px; }
|
||||||
|
.greatest-period { padding: 20px 0 28px; border-top: 2px solid #48404a; }
|
||||||
|
.greatest-period-heading { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 16px; }
|
||||||
|
.greatest-period-heading h2 { margin: 0; font-size: 21px; }
|
||||||
|
.greatest-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: 22px 16px; }
|
||||||
|
.greatest-tile { min-width: 0; }
|
||||||
|
.greatest-open { position: relative; display: block; width: 100%; padding: 0; border: 0; text-align: left; color: inherit; background: none; cursor: pointer; font: inherit; }
|
||||||
|
.greatest-open:hover .greatest-cover img { transform: scale(1.035); }
|
||||||
|
.greatest-open:focus-visible, .greatest-vote:focus-visible { outline: 2px solid #fff; outline-offset: 3px; }
|
||||||
|
.greatest-cover { position: relative; display: block; width: 100%; aspect-ratio: 2 / 3; overflow: hidden; border-radius: 9px; background: linear-gradient(150deg, #49343c, #282c38 60%, #18181d); box-shadow: 0 4px 17px #0007; }
|
||||||
|
.greatest-cover img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; transition: transform .2s; }
|
||||||
|
.greatest-cover-fallback { display: grid; place-items: center; width: 100%; height: 100%; color: #dbc8bd; font: 700 80px/1 Georgia, serif; opacity: .6; }
|
||||||
|
.greatest-owned { position: absolute; top: 7px; right: 7px; padding: 2px 7px; border-radius: 9px; background: #183d34; color: #a7f0d1; font-size: 11px; font-weight: 700; box-shadow: 0 1px 8px #0008; }
|
||||||
|
.greatest-name { display: block; margin-top: 8px; font-weight: 700; line-height: 1.25; }
|
||||||
|
.greatest-meta { display: block; margin-top: 2px; color: var(--muted); font-size: 12px; }
|
||||||
|
.greatest-votes { display: flex; gap: 8px; margin-top: 12px; }
|
||||||
|
.greatest-votes.compact { gap: 3px; margin-top: 6px; }
|
||||||
|
.greatest-vote { display: inline-grid; place-items: center; flex: 1; min-width: 0; min-height: 36px; border: 1px solid var(--line); border-radius: 8px; background: var(--card); color: var(--fg); cursor: pointer; font: inherit; }
|
||||||
|
.greatest-vote:hover { background: var(--card2); }
|
||||||
|
.greatest-vote.selected { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 25%, var(--card)); }
|
||||||
|
.greatest-vote:disabled { opacity: .5; cursor: wait; }
|
||||||
|
.greatest-dialog { width: min(780px, calc(100vw - 24px)); max-height: min(850px, calc(100vh - 30px)); overflow: auto; padding: 18px; border: 1px solid var(--line); border-radius: 14px; background: var(--card); color: var(--fg); box-shadow: 0 18px 70px #000a; }
|
||||||
|
.greatest-dialog::backdrop { background: #000b; }
|
||||||
|
.greatest-dialog-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 14px; }
|
||||||
|
.greatest-dialog-body { display: grid; grid-template-columns: minmax(130px, 230px) minmax(0, 1fr); gap: 22px; }
|
||||||
|
.greatest-dialog-body h2 { margin: 0 0 8px; font-size: 25px; }
|
||||||
|
.greatest-dialog-body p { margin: 9px 0; }
|
||||||
|
.greatest-links { display: flex; flex-wrap: wrap; gap: 8px; margin: 17px 0; }
|
||||||
|
@media (max-width: 680px) {
|
||||||
|
.greatest-toolbar { align-items: stretch; flex-direction: column; }
|
||||||
|
.greatest-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px 12px; }
|
||||||
|
.greatest-dialog-body { grid-template-columns: 1fr; }
|
||||||
|
.greatest-dialog-body > .greatest-cover { width: min(160px, 42vw); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||||
|
<title>Shijō Saikō no Manga · MangaCircus</title>
|
||||||
|
<link rel="stylesheet" href="app.css">
|
||||||
|
<link rel="stylesheet" href="greatest.css">
|
||||||
|
<link rel="icon" href="icon.svg">
|
||||||
|
<meta name="theme-color" content="#121214">
|
||||||
|
<script src="common.js" defer></script>
|
||||||
|
<script src="greatest.js" defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main><p class="loading">Loading the collection…</p></main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
'use strict';
|
||||||
|
/* Curated catalog UI. Rating is the only write; the library and scraper are untouched. */
|
||||||
|
(async function () {
|
||||||
|
let me, data;
|
||||||
|
try { [me, data] = await Promise.all([api('api/me'), api('api/greatest')]); } catch (error) { return showError(error); }
|
||||||
|
topNav(me);
|
||||||
|
const main = $('main'); main.textContent = '';
|
||||||
|
let sortMode = store.get('greatest-sort') === 'all' ? 'all' : 'mine';
|
||||||
|
let filterMode = 'all';
|
||||||
|
const query = h('input', { type: 'search', placeholder: 'Search the collection…', 'aria-label': 'Search Greatest of All Time' });
|
||||||
|
const count = h('span', { class: 'muted small', 'aria-live': 'polite' });
|
||||||
|
const mine = h('button', { type: 'button', class: 'btn small' }, 'Rank by my vote');
|
||||||
|
const everyone = h('button', { type: 'button', class: 'btn small' }, 'Rank by everyone');
|
||||||
|
const filterBar = h('div', { class: 'greatest-filters', 'aria-label': 'Filter by your rating' });
|
||||||
|
const filters = [
|
||||||
|
['all', 'All'], ['rated', 'Rated'], ['flame', '🔥 Flame'], ['good', '👍 Looks good'],
|
||||||
|
['neutral', '● Neutral'], ['skip', '💀 Not for me'], ['unrated', 'Unrated'], ['owned', '📚 In library'],
|
||||||
|
];
|
||||||
|
const filterButtons = new Map();
|
||||||
|
for (const [value, label] of filters) {
|
||||||
|
const button = h('button', { type: 'button', class: 'filter-chip' }, label);
|
||||||
|
button.onclick = () => { filterMode = value; render(); };
|
||||||
|
filterButtons.set(value, button); filterBar.append(button);
|
||||||
|
}
|
||||||
|
const results = h('div', { class: 'greatest-periods' });
|
||||||
|
const dialog = h('dialog', { class: 'greatest-dialog', 'aria-label': 'Manga details' });
|
||||||
|
document.body.append(dialog);
|
||||||
|
mine.onclick = () => { sortMode = 'mine'; store.set('greatest-sort', sortMode); render(); };
|
||||||
|
everyone.onclick = () => { sortMode = 'all'; store.set('greatest-sort', sortMode); render(); };
|
||||||
|
query.oninput = render;
|
||||||
|
main.append(
|
||||||
|
h('div', { class: 'greatest-intro' },
|
||||||
|
h('p', { class: 'greatest-eyebrow' }, 'Greatest of All Time'),
|
||||||
|
h('h1', {}, data.title || 'Shijō Saikō no Manga'),
|
||||||
|
h('p', { class: 'muted' }, '史上最高の漫画 · A curated journey through manga history. Your votes are private to your account; the community ranking combines everyone’s votes.'),
|
||||||
|
h('p', { class: 'muted small' }, '🔥 Definitely want · 👍 Looks good · ● Neutral · 💀 Not for me · 📚 Already in your library')),
|
||||||
|
h('div', { class: 'greatest-toolbar' }, query, h('div', { class: 'greatest-sort' }, mine, everyone)),
|
||||||
|
filterBar, count, results);
|
||||||
|
|
||||||
|
function ratingButtons(item, compact = false) {
|
||||||
|
const bar = h('div', { class: 'greatest-votes' + (compact ? ' compact' : ''), 'aria-label': `Rate ${item.title}` });
|
||||||
|
for (const [value, symbol, label] of [
|
||||||
|
['flame', '🔥', 'Definitely want'], ['good', '👍', 'Looks good'],
|
||||||
|
['neutral', '●', 'Neutral'], ['skip', '💀', 'Not for me'],
|
||||||
|
]) {
|
||||||
|
const selected = item.myRating === value;
|
||||||
|
const button = h('button', { type: 'button', class: 'greatest-vote' + (selected ? ' selected' : ''),
|
||||||
|
'aria-label': `${label}: ${item.title}`, 'aria-pressed': String(selected), title: label }, symbol);
|
||||||
|
button.onclick = async event => {
|
||||||
|
event.stopPropagation(); button.disabled = true;
|
||||||
|
try {
|
||||||
|
const next = selected ? null : value;
|
||||||
|
const saved = await api('api/greatest/rating', { id: item.id, rating: next });
|
||||||
|
Object.assign(item, saved);
|
||||||
|
render();
|
||||||
|
if (dialog.open) openDetails(item);
|
||||||
|
} catch (error) { alert(error.message || String(error)); button.disabled = false; }
|
||||||
|
};
|
||||||
|
bar.append(button);
|
||||||
|
}
|
||||||
|
return bar;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cover(item) {
|
||||||
|
const box = h('span', { class: 'greatest-cover' }, h('span', { class: 'greatest-cover-fallback', 'aria-hidden': 'true' }, item.title.slice(0, 1)));
|
||||||
|
if (item.cover && safeHref(item.cover)) {
|
||||||
|
const image = h('img', { src: item.cover, alt: '', loading: 'lazy' });
|
||||||
|
image.onerror = () => image.remove();
|
||||||
|
box.append(image);
|
||||||
|
}
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tile(item) {
|
||||||
|
const open = h('button', { type: 'button', class: 'greatest-open', 'aria-label': `Details for ${item.title}` },
|
||||||
|
cover(item),
|
||||||
|
item.library ? h('span', { class: 'greatest-owned', title: 'Already in your library' }, '📚 In library') : null,
|
||||||
|
h('span', { class: 'greatest-name' }, item.title),
|
||||||
|
h('span', { class: 'greatest-meta' }, `${item.year} · ${item.chapters == null ? 'Chapters unknown' : `${item.chapters} chapters`}`),
|
||||||
|
h('span', { class: 'greatest-meta' }, statusLabel(item.status)));
|
||||||
|
open.onclick = () => openDetails(item);
|
||||||
|
return h('article', { class: 'greatest-tile' }, open, ratingButtons(item, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusLabel(status) {
|
||||||
|
return status === 'ongoing' ? 'Ongoing' : status === 'completed' ? 'Completed' : 'Status unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDetails(item) {
|
||||||
|
dialog.textContent = '';
|
||||||
|
const close = h('button', { type: 'button', class: 'btn small', onclick: () => dialog.close() }, 'Close');
|
||||||
|
const links = h('div', { class: 'greatest-links' });
|
||||||
|
if (item.library) links.append(h('a', { class: 'btn primary', href: mangaUrl(item.library.id) }, 'Open in library'));
|
||||||
|
if (safeHref(item.source?.url)) links.append(h('a', { class: 'btn', href: item.source.url, target: '_blank', rel: 'noopener noreferrer' },
|
||||||
|
`Official ${item.source.kind || 'source'} ↗`));
|
||||||
|
if (safeHref(item.metadata?.url)) links.append(h('a', { class: 'btn', href: item.metadata.url, target: '_blank', rel: 'noopener noreferrer' }, 'AniList metadata ↗'));
|
||||||
|
const votes = item.votes || {};
|
||||||
|
dialog.append(
|
||||||
|
h('div', { class: 'greatest-dialog-head' }, h('span', { class: 'muted small' }, `${item.period} · curator #${item.rank}`), close),
|
||||||
|
h('div', { class: 'greatest-dialog-body' }, cover(item), h('div', {},
|
||||||
|
h('h2', {}, item.title),
|
||||||
|
h('p', { class: 'muted' }, `${item.year} · ${item.chapters == null ? 'Chapter count unknown' : `${item.chapters} chapters`} · ${statusLabel(item.status)}`),
|
||||||
|
h('p', {}, String(item.note || '').replace(/\*/g, '')),
|
||||||
|
h('p', { class: 'muted small' }, `Community votes: 🔥 ${votes.flame || 0} · 👍 ${votes.good || 0} · ● ${votes.neutral || 0} · 💀 ${votes.skip || 0}`),
|
||||||
|
ratingButtons(item), links,
|
||||||
|
h('p', { class: 'muted small' }, 'Chapter counts and status come from public catalog metadata, or from your local library when present. Availability may vary by region.'))));
|
||||||
|
if (!dialog.open) dialog.showModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
function matches(item, q) {
|
||||||
|
if (q && !`${item.title} ${item.note || ''}`.toLowerCase().includes(q)) return false;
|
||||||
|
if (filterMode === 'rated') return !!item.myRating;
|
||||||
|
if (filterMode === 'unrated') return !item.myRating;
|
||||||
|
if (filterMode === 'owned') return !!item.library;
|
||||||
|
if (['flame', 'good', 'neutral', 'skip'].includes(filterMode)) return item.myRating === filterMode;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
mine.classList.toggle('on', sortMode === 'mine'); everyone.classList.toggle('on', sortMode === 'all');
|
||||||
|
mine.setAttribute('aria-pressed', String(sortMode === 'mine'));
|
||||||
|
everyone.setAttribute('aria-pressed', String(sortMode === 'all'));
|
||||||
|
for (const [value, button] of filterButtons) {
|
||||||
|
button.classList.toggle('on', filterMode === value);
|
||||||
|
button.setAttribute('aria-pressed', String(filterMode === value));
|
||||||
|
}
|
||||||
|
results.textContent = '';
|
||||||
|
const q = query.value.trim().toLowerCase();
|
||||||
|
let visible = 0;
|
||||||
|
for (const period of data.periods) {
|
||||||
|
const items = data.entries.filter(item => item.period === period && matches(item, q));
|
||||||
|
if (!items.length) continue;
|
||||||
|
items.sort((a, b) => sortMode === 'all'
|
||||||
|
? b.score - a.score || a.rank - b.rank
|
||||||
|
: (ratingScore(b.myRating) - ratingScore(a.myRating)) || a.rank - b.rank);
|
||||||
|
visible += items.length;
|
||||||
|
results.append(h('section', { class: 'greatest-period' },
|
||||||
|
h('div', { class: 'greatest-period-heading' }, h('h2', {}, period), h('span', { class: 'muted small' }, `${items.length} manga`)),
|
||||||
|
h('div', { class: 'greatest-grid' }, items.map(tile))));
|
||||||
|
}
|
||||||
|
count.textContent = `${visible} of ${data.entries.length} manga · ${sortMode === 'all' ? 'community score' : 'your vote'} within each period`;
|
||||||
|
if (!visible) results.append(h('p', { class: 'empty muted' }, 'No manga match these filters.'));
|
||||||
|
}
|
||||||
|
function ratingScore(value) { return { flame: 3, good: 1, neutral: 0, skip: -2 }[value] ?? -3; }
|
||||||
|
render();
|
||||||
|
})();
|
||||||
@@ -14,6 +14,8 @@
|
|||||||
const http = require('http'), fs = require('fs'), path = require('path'), crypto = require('crypto');
|
const http = require('http'), fs = require('fs'), path = require('path'), crypto = require('crypto');
|
||||||
const { scanLibrary, scanChangedLibrary, libraryStamps, snapshotIndex, restoreIndex } = require('./library');
|
const { scanLibrary, scanChangedLibrary, libraryStamps, snapshotIndex, restoreIndex } = require('./library');
|
||||||
const { Catalog, CatalogError, collectionViews, TYPES, ROLES } = require('./catalog');
|
const { Catalog, CatalogError, collectionViews, TYPES, ROLES } = require('./catalog');
|
||||||
|
const { RATINGS: GREATEST_RATINGS, loadCatalog: loadGreatestCatalog, findOwned: findGreatestOwned,
|
||||||
|
countsFor: greatestCounts, scoreFor: greatestScore } = require('./greatest');
|
||||||
const { secret } = require('./runtime/config');
|
const { secret } = require('./runtime/config');
|
||||||
const {
|
const {
|
||||||
applyPulse, applyVisit, chapterInsight, eligiblePages, emptyRecord, libraryInsight, mangaInsight, publicSummary,
|
applyPulse, applyVisit, chapterInsight, eligiblePages, emptyRecord, libraryInsight, mangaInsight, publicSummary,
|
||||||
@@ -40,8 +42,10 @@ const MIME = {
|
|||||||
// ---------- storage: one JSON file, written atomically ----------
|
// ---------- storage: one JSON file, written atomically ----------
|
||||||
fs.mkdirSync(DATA, { recursive: true });
|
fs.mkdirSync(DATA, { recursive: true });
|
||||||
const catalog = new Catalog(DATA);
|
const catalog = new Catalog(DATA);
|
||||||
|
const greatestCatalog = loadGreatestCatalog();
|
||||||
|
const greatestById = new Map(greatestCatalog.entries.map(entry => [entry.id, entry]));
|
||||||
const DB_FILE = path.join(DATA, 'db.json'), CACHE_FILE = path.join(DATA, 'scan-cache.json'), INDEX_FILE = path.join(DATA, 'library-index.json');
|
const DB_FILE = path.join(DATA, 'db.json'), CACHE_FILE = path.join(DATA, 'scan-cache.json'), INDEX_FILE = path.join(DATA, 'library-index.json');
|
||||||
const db = { users: {}, sessions: {}, progress: {}, reading: {}, follows: {}, notes: {}, votes: {}, known: {} };
|
const db = { users: {}, sessions: {}, progress: {}, reading: {}, follows: {}, notes: {}, votes: {}, greatestRatings: {}, known: {} };
|
||||||
// A corrupt db.json stops the server instead of silently starting empty and overwriting it.
|
// A corrupt db.json stops the server instead of silently starting empty and overwriting it.
|
||||||
if (fs.existsSync(DB_FILE)) Object.assign(db, JSON.parse(fs.readFileSync(DB_FILE, 'utf8')));
|
if (fs.existsSync(DB_FILE)) Object.assign(db, JSON.parse(fs.readFileSync(DB_FILE, 'utf8')));
|
||||||
function writeAtomic(file, text) { fs.writeFileSync(file + '.tmp', text); fs.renameSync(file + '.tmp', file); }
|
function writeAtomic(file, text) { fs.writeFileSync(file + '.tmp', text); fs.renameSync(file + '.tmp', file); }
|
||||||
@@ -364,6 +368,36 @@ const routes = {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'GET /api/greatest': ({ u }) => ({
|
||||||
|
title: greatestCatalog.title, updatedAt: greatestCatalog.updatedAt, periods: greatestCatalog.periods,
|
||||||
|
ratings: GREATEST_RATINGS,
|
||||||
|
entries: greatestCatalog.entries.map(entry => {
|
||||||
|
const owned = findGreatestOwned(entry, lib.mangas);
|
||||||
|
const counts = greatestCounts(entry.id, db.greatestRatings || {}, db.users);
|
||||||
|
return {
|
||||||
|
...entry,
|
||||||
|
cover: owned?.cover ? url(owned.cover) : entry.cover,
|
||||||
|
chapters: owned ? owned.chapters.length : entry.chapters,
|
||||||
|
status: owned?.meta?.status || entry.status,
|
||||||
|
library: owned ? { id: owned.id, chapters: owned.chapters.length } : null,
|
||||||
|
myRating: db.greatestRatings?.[u.name]?.[entry.id] || null,
|
||||||
|
votes: counts, score: greatestScore(counts),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'POST /api/greatest/rating': ({ u, b }) => {
|
||||||
|
const id = String(b.id || '');
|
||||||
|
if (!greatestById.has(id)) bad(404, 'Catalog entry not found');
|
||||||
|
const rating = b.rating === null || b.rating === '' ? null : String(b.rating || '');
|
||||||
|
if (rating !== null && !GREATEST_RATINGS.includes(rating)) bad(400, 'Unknown rating');
|
||||||
|
const all = (db.greatestRatings = db.greatestRatings || {});
|
||||||
|
const mine = (all[u.name] = all[u.name] || {});
|
||||||
|
if (rating === null) delete mine[id]; else mine[id] = rating;
|
||||||
|
save();
|
||||||
|
const votes = greatestCounts(id, all, db.users);
|
||||||
|
return { id, myRating: rating, votes, score: greatestScore(votes) };
|
||||||
|
},
|
||||||
|
|
||||||
'GET /api/manga': ({ u, q }) => {
|
'GET /api/manga': ({ u, q }) => {
|
||||||
const m = mangaOf(q.get('m')), prog = progressOf(u, m), showStory = settingsOf(u).readingStory;
|
const m = mangaOf(q.get('m')), prog = progressOf(u, m), showStory = settingsOf(u).readingStory;
|
||||||
const records = mangaReadingOf(u, m);
|
const records = mangaReadingOf(u, m);
|
||||||
@@ -545,6 +579,7 @@ const routes = {
|
|||||||
const name = String(b.name || '');
|
const name = String(b.name || '');
|
||||||
if (name === u.name) bad(400, "You can't delete your own account.");
|
if (name === u.name) bad(400, "You can't delete your own account.");
|
||||||
delete db.users[name]; delete db.progress[name]; delete db.reading[name]; delete db.follows[name]; delete db.notes[name];
|
delete db.users[name]; delete db.progress[name]; delete db.reading[name]; delete db.follows[name]; delete db.notes[name];
|
||||||
|
if (db.greatestRatings) delete db.greatestRatings[name];
|
||||||
for (const [k, s] of Object.entries(db.sessions)) if (s.u === name) delete db.sessions[k];
|
for (const [k, s] of Object.entries(db.sessions)) if (s.u === name) delete db.sessions[k];
|
||||||
save();
|
save();
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
'use strict';
|
||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const { loadCatalog, findOwned, countsFor, scoreFor } = require('../greatest');
|
||||||
|
|
||||||
|
test('curated manga catalog has unique identities and ordered five-year periods', () => {
|
||||||
|
const catalog = loadCatalog();
|
||||||
|
assert.equal(catalog.periods.length, 13);
|
||||||
|
assert.equal(catalog.entries.length, 106);
|
||||||
|
for (const [index, period] of catalog.periods.entries()) {
|
||||||
|
const entries = catalog.entries.filter(entry => entry.period === period);
|
||||||
|
assert.ok(entries.length >= 5 && entries.length <= 10);
|
||||||
|
assert.deepEqual(entries.map(entry => entry.rank), entries.map((_, rank) => rank + 1));
|
||||||
|
assert.ok(entries.every(entry => entry.periodOrder === index));
|
||||||
|
}
|
||||||
|
assert.equal(catalog.entries.find(entry => entry.title === 'Phoenix').year, 1954);
|
||||||
|
assert.equal(catalog.entries.find(entry => entry.title === 'JoJo\'s Bizarre Adventure').year, 1987);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('library matching and vote aggregation stay independent of scraper data', () => {
|
||||||
|
const catalog = loadCatalog();
|
||||||
|
const berserk = catalog.entries.find(entry => entry.title === 'Berserk');
|
||||||
|
const manga = { id: 'local-Berserk', title: 'Berserk', meta: {} };
|
||||||
|
assert.equal(findOwned(berserk, [manga]), manga);
|
||||||
|
assert.equal(findOwned(berserk, [{ id: 'different', title: 'Berserker', meta: {} }]), null);
|
||||||
|
const votes = { ada: { [berserk.id]: 'flame' }, bob: { [berserk.id]: 'good' }, gone: { [berserk.id]: 'skip' } };
|
||||||
|
const counts = countsFor(berserk.id, votes, { ada: {}, bob: {} });
|
||||||
|
assert.deepEqual(counts, { flame: 1, good: 1, neutral: 0, skip: 0 });
|
||||||
|
assert.equal(scoreFor(counts), 4);
|
||||||
|
});
|
||||||
@@ -47,6 +47,32 @@ test('three services integrate collections, auth, custom covers and private heal
|
|||||||
let inventory;
|
let inventory;
|
||||||
for (let n = 0; n < 50; n++) { inventory = await (await request('/api/library')).json(); if (inventory.mangas.length === 2) break; await delay(100); }
|
for (let n = 0; n < 50; n++) { inventory = await (await request('/api/library')).json(); if (inventory.mangas.length === 2) break; await delay(100); }
|
||||||
assert.equal(inventory.mangas.length, 2);
|
assert.equal(inventory.mangas.length, 2);
|
||||||
|
assert.equal((await fetch(base + '/api/greatest')).status, 401);
|
||||||
|
const greatest = await (await request('/api/greatest')).json();
|
||||||
|
assert.equal(greatest.entries.length, 106);
|
||||||
|
const firstPick = greatest.entries[0];
|
||||||
|
assert.equal(firstPick.myRating, null);
|
||||||
|
assert.equal((await request('/api/greatest/rating', { id: firstPick.id, rating: 'flame' })).status, 200);
|
||||||
|
assert.equal((await request('/api/greatest/rating', { id: firstPick.id, rating: 'invalid' })).status, 400);
|
||||||
|
assert.equal((await request('/api/greatest/rating', { id: 'missing', rating: 'good' })).status, 404);
|
||||||
|
const afterRating = (await (await request('/api/greatest')).json()).entries[0];
|
||||||
|
assert.equal(afterRating.myRating, 'flame');
|
||||||
|
assert.equal(afterRating.votes.flame, 1);
|
||||||
|
assert.equal(afterRating.score, 3);
|
||||||
|
assert.equal((await request('/api/admin/user', { name: 'another', password: 'local-test-password' })).status, 200);
|
||||||
|
const anotherLogin = await fetch(base + '/api/login', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name: 'another', password: 'local-test-password' }) });
|
||||||
|
assert.equal(anotherLogin.status, 200);
|
||||||
|
const anotherCookie = anotherLogin.headers.get('set-cookie').split(';')[0];
|
||||||
|
const anotherRequest = (endpoint, body) => fetch(base + endpoint, { headers: { cookie: anotherCookie, 'content-type': 'application/json' }, ...(body ? { method: 'POST', body: JSON.stringify(body) } : {}) });
|
||||||
|
assert.equal((await (await anotherRequest('/api/greatest')).json()).entries[0].myRating, null);
|
||||||
|
assert.equal((await anotherRequest('/api/greatest/rating', { id: firstPick.id, rating: 'good' })).status, 200);
|
||||||
|
const communityPick = (await (await request('/api/greatest')).json()).entries[0];
|
||||||
|
assert.equal(communityPick.myRating, 'flame');
|
||||||
|
assert.deepEqual(communityPick.votes, { flame: 1, good: 1, neutral: 0, skip: 0 });
|
||||||
|
assert.equal((await request('/api/greatest/rating', { id: firstPick.id, rating: null })).status, 200);
|
||||||
|
assert.equal((await (await request('/api/greatest')).json()).entries[0].votes.flame, 0);
|
||||||
|
assert.equal((await request('/api/admin/user/delete', { name: 'another' })).status, 200);
|
||||||
|
assert.equal((await (await request('/api/greatest')).json()).entries[0].votes.good, 0);
|
||||||
const ids = inventory.mangas.map(m => m.id);
|
const ids = inventory.mangas.map(m => m.id);
|
||||||
const catalog = { revision: 0, collections: [{ id: 'demo', title: 'Demo collection', members: ids.map((mangaId, index) => ({ mangaId, readingOrder: index + 1, role: index ? 'sequel' : 'main' })), coverMode: 'mosaic' }], relations: [{ from: ids[0], to: ids[1], type: 'sequel', state: 'confirmed' }] };
|
const catalog = { revision: 0, collections: [{ id: 'demo', title: 'Demo collection', members: ids.map((mangaId, index) => ({ mangaId, readingOrder: index + 1, role: index ? 'sequel' : 'main' })), coverMode: 'mosaic' }], relations: [{ from: ids[0], to: ids[1], type: 'sequel', state: 'confirmed' }] };
|
||||||
let response = await request('/api/admin/catalog', catalog); assert.equal(response.status, 200); const saved = await response.json();
|
let response = await request('/api/admin/catalog', catalog); assert.equal(response.status, 200); const saved = await response.json();
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
'use strict';
|
||||||
|
// Optional, manually run metadata pass. The reader never contacts AniList at runtime.
|
||||||
|
// This intentionally only enriches an exact first-serialization-year match; a sequel,
|
||||||
|
// remake, or similarly named work is worse than a missing cover.
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { normalize } = require('../greatest');
|
||||||
|
|
||||||
|
const file = path.join(__dirname, '..', 'greatest', 'catalog.json');
|
||||||
|
const catalog = JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||||
|
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
const query = `query ($search: String) {
|
||||||
|
Page(perPage: 10) {
|
||||||
|
media(search: $search, type: MANGA, format: MANGA, sort: POPULARITY_DESC) {
|
||||||
|
id title { english romaji native } startDate { year } chapters status siteUrl
|
||||||
|
coverImage { large }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
const aliases = {
|
||||||
|
'kitaro-gegege-no-kitaro-1960': 'Hakaba Kitarou',
|
||||||
|
'the-legend-of-kamui-kamui-den-1964': 'Kamui Den',
|
||||||
|
'the-poem-of-wind-and-trees-1976': 'Kaze to Ki no Uta',
|
||||||
|
'the-heart-of-thomas-1974': 'Thomas no Shinzou',
|
||||||
|
'gto-great-teacher-onizuka-1997': 'Great Teacher Onizuka',
|
||||||
|
'someone-hertz-2025': 'Samawan Hertz',
|
||||||
|
'home-at-the-horizon-2025': 'Okaeri Suiheisen',
|
||||||
|
'war-of-the-adults-2025': 'Otona Taisen',
|
||||||
|
'frieren-beyond-journey-s-end-2020': 'Sousou no Frieren',
|
||||||
|
};
|
||||||
|
|
||||||
|
async function search(title) {
|
||||||
|
for (let attempt = 0; attempt < 3; attempt++) {
|
||||||
|
const response = await fetch('https://graphql.anilist.co', {
|
||||||
|
method: 'POST', headers: { 'content-type': 'application/json' },
|
||||||
|
body: JSON.stringify({ query, variables: { search: title } }), signal: AbortSignal.timeout(12000),
|
||||||
|
});
|
||||||
|
if (response.status === 429 || response.status >= 500) { await wait(10000 * (attempt + 1)); continue; }
|
||||||
|
const body = await response.json();
|
||||||
|
return body.data?.Page?.media || [];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
let matched = 0;
|
||||||
|
for (const [index, entry] of catalog.entries.entries()) {
|
||||||
|
const title = aliases[entry.id] || entry.title.split(' / ')[0].split(': ')[0];
|
||||||
|
try {
|
||||||
|
const results = await search(title);
|
||||||
|
const target = normalize(title);
|
||||||
|
const match = results.find(item => item.startDate?.year === entry.year &&
|
||||||
|
[item.title?.english, item.title?.romaji, item.title?.native].some(name => normalize(name) === target)) ||
|
||||||
|
results.find(item => item.startDate?.year === entry.year &&
|
||||||
|
[item.title?.english, item.title?.romaji].some(name => normalize(name).includes(target) || target.includes(normalize(name))));
|
||||||
|
if (match) {
|
||||||
|
entry.cover = /^https:\/\//.test(match.coverImage?.large || '') ? match.coverImage.large : null;
|
||||||
|
entry.chapters = Number.isInteger(match.chapters) && match.chapters > 0 ? match.chapters : null;
|
||||||
|
entry.status = match.status === 'RELEASING' ? 'ongoing' : match.status === 'FINISHED' ? 'completed' : 'unknown';
|
||||||
|
entry.metadata = { provider: 'AniList', id: match.id, url: match.siteUrl };
|
||||||
|
matched++;
|
||||||
|
} else process.stderr.write(`No exact year/title match: ${entry.title} (${entry.year})\n`);
|
||||||
|
} catch (error) { process.stderr.write(`Metadata unavailable: ${entry.title}: ${error.message}\n`); }
|
||||||
|
if ((index + 1) % 10 === 0) process.stderr.write(`Checked ${index + 1}/${catalog.entries.length}; matched ${matched}\n`);
|
||||||
|
await wait(1500);
|
||||||
|
}
|
||||||
|
fs.writeFileSync(file + '.tmp', JSON.stringify(catalog, null, 2) + '\n');
|
||||||
|
fs.renameSync(file + '.tmp', file);
|
||||||
|
process.stderr.write(`Saved ${matched}/${catalog.entries.length} exact matches\n`);
|
||||||
|
})().catch(error => { console.error(error); process.exitCode = 1; });
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
'use strict';
|
||||||
|
// Disposable local reader + browser; no production accounts or scraper traffic.
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs'), os = require('node:os'), path = require('node:path'), net = require('node:net');
|
||||||
|
const { spawn } = require('node:child_process');
|
||||||
|
const { chromium } = require('playwright');
|
||||||
|
const root = path.resolve(__dirname, '..');
|
||||||
|
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
async function freePort() {
|
||||||
|
const server = net.createServer();
|
||||||
|
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
||||||
|
const port = server.address().port;
|
||||||
|
await new Promise(resolve => server.close(resolve));
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mc-greatest-ui-'));
|
||||||
|
const library = path.join(dir, 'library'), data = path.join(dir, 'data');
|
||||||
|
fs.mkdirSync(library); fs.mkdirSync(data);
|
||||||
|
const port = await freePort(), base = `http://127.0.0.1:${port}`;
|
||||||
|
const child = spawn(process.execPath, ['server.js'], { cwd: root, windowsHide: true,
|
||||||
|
env: { ...process.env, PORT: String(port), LIBRARY: library, DATA: data, SCAN_INTERVAL: '0',
|
||||||
|
FULL_SCAN_INTERVAL: '0', SCRAPER_URL: 'http://127.0.0.1:1', COOKIE_SECURE: '0' }, stdio: 'ignore' });
|
||||||
|
let browser;
|
||||||
|
try {
|
||||||
|
for (let i = 0; i < 80; i++) {
|
||||||
|
if (await fetch(base + '/health').then(r => r.ok).catch(() => false)) break;
|
||||||
|
await delay(100);
|
||||||
|
}
|
||||||
|
assert.equal((await fetch(base + '/health')).status, 200);
|
||||||
|
const signup = await fetch(base + '/api/signup', { method: 'POST', headers: { 'content-type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name: 'curator', password: 'local-test-password' }) });
|
||||||
|
assert.equal(signup.status, 200);
|
||||||
|
const cookie = signup.headers.get('set-cookie').split(';')[0];
|
||||||
|
browser = await chromium.launch({ channel: process.env.QA_BROWSER_CHANNEL || 'msedge', headless: true });
|
||||||
|
const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
||||||
|
const [name, value] = cookie.split('=');
|
||||||
|
await context.addCookies([{ name, value, url: base }]);
|
||||||
|
await context.route(/^https:\/\//, route => route.abort()); // Never load remote cover art in fixture.
|
||||||
|
const page = await context.newPage(), errors = [];
|
||||||
|
page.on('pageerror', error => errors.push(error.message));
|
||||||
|
await page.goto(base + '/greatest.html');
|
||||||
|
await page.getByRole('heading', { name: 'Shijō Saikō no Manga' }).waitFor();
|
||||||
|
assert.equal(await page.locator('.greatest-period').count(), 13);
|
||||||
|
assert.equal(await page.locator('.greatest-tile').count(), 106);
|
||||||
|
const astro = page.locator('.greatest-tile').filter({ hasText: 'Astro Boy' });
|
||||||
|
await astro.getByRole('button', { name: 'Definitely want: Astro Boy' }).click();
|
||||||
|
await page.getByRole('button', { name: '🔥 Flame' }).click();
|
||||||
|
assert.equal(await page.locator('.greatest-tile').count(), 1);
|
||||||
|
await page.getByRole('button', { name: 'All', exact: true }).click();
|
||||||
|
await page.getByRole('button', { name: 'Rank by everyone' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Details for Astro Boy' }).click();
|
||||||
|
await page.getByRole('dialog').getByText('Community votes: 🔥 1').waitFor();
|
||||||
|
await page.getByRole('dialog').getByRole('button', { name: 'Close' }).click();
|
||||||
|
await page.setViewportSize({ width: 390, height: 844 });
|
||||||
|
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), 'Mobile horizontal overflow');
|
||||||
|
await page.getByRole('button', { name: '📚 In library' }).click();
|
||||||
|
assert.equal(await page.locator('.greatest-tile').count(), 0);
|
||||||
|
assert.deepEqual(errors, []);
|
||||||
|
await context.close();
|
||||||
|
console.log('Greatest of All Time browser QA passed: periods, ratings, filters, modal, mobile layout.');
|
||||||
|
} finally {
|
||||||
|
if (browser) await browser.close();
|
||||||
|
await new Promise(resolve => { if (child.exitCode !== null) return resolve(); child.once('exit', resolve); child.kill(); });
|
||||||
|
fs.rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
})().catch(error => { console.error(error); process.exitCode = 1; });
|
||||||
Reference in new issue
Block a user