diff --git a/.claude/launch.json b/.claude/launch.json index 95ef42c..c541bb7 100644 --- a/.claude/launch.json +++ b/.claude/launch.json @@ -2,7 +2,7 @@ "version": "0.0.1", "configurations": [ { - "name": "mangaparty", + "name": "mangacircus", "runtimeExecutable": "node", "runtimeArgs": ["server.js"], "port": 8080 diff --git a/.gitignore b/.gitignore index 3f05199..7d47eff 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -# --- MangaParty runtime data (never commit) --- +# --- MangaCircus runtime data (never commit) --- # user accounts (password hashes), sessions, progress, scan cache /data/ # the manga library: demo content is generated by `node tools/make-dummy.js`, a real one is yours alone diff --git a/README.md b/README.md index 13f2f92..86ebb4e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# MangaParty +# MangaCircus A no-frills, self-hosted manga / comic / webtoon reader. @@ -25,7 +25,7 @@ node tools/make-dummy.js library docker compose up -d --build ``` -`docker-compose.yml` mounts `./library` read-only at `/library` and keeps accounts/progress in the `mangaparty-data` volume (`/data`). +`docker-compose.yml` mounts `./library` read-only at `/library` and keeps accounts/progress in the `mangacircus-data` volume (`/data`). ### Settings (environment variables) diff --git a/docker-compose.yml b/docker-compose.yml index 789752b..b8233df 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,15 +1,15 @@ services: - mangaparty: + mangacircus: build: . ports: - "8080:8080" volumes: - ./library:/library:ro # your manga folders (read-only is fine) - - mangaparty-data:/data # users, progress, votes, scan cache + - mangacircus-data:/data # users, progress, votes, scan cache environment: SIGNUP: closed # "open" lets anyone create an account SCAN_INTERVAL: 10 # minutes between automatic rescans (0 = off) restart: unless-stopped volumes: - mangaparty-data: + mangacircus-data: diff --git a/docs/architecture.md b/docs/architecture.md index 481dbcf..d481b39 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,6 +1,6 @@ -# MangaParty architecture +# MangaCircus architecture -MangaParty is a self-hosted reader for manga, comics and webtoons. One Node.js process serves a handful of static pages and a small JSON API. The content is a folder tree on disk, and everything user-related lives in one JSON file. +MangaCircus is a self-hosted reader for manga, comics and webtoons. One Node.js process serves a handful of static pages and a small JSON API. The content is a folder tree on disk, and everything user-related lives in one JSON file. Related documents: - [library-format.md](library-format.md): the folder format. diff --git a/docs/import.md b/docs/import.md index 90dffca..ae82c37 100644 --- a/docs/import.md +++ b/docs/import.md @@ -1,6 +1,6 @@ # How the import works -MangaParty does not copy or convert anything. "Import" means **indexing the library folder in place**. The scanner (`library.js`) walks the folder tree described in [library-format.md](library-format.md) and builds an in-memory index. The server (`server.js`) swaps that index in, compares it with what it has seen before, and notifies followers of new chapters. The library itself is only ever read. +MangaCircus does not copy or convert anything. "Import" means **indexing the library folder in place**. The scanner (`library.js`) walks the folder tree described in [library-format.md](library-format.md) and builds an in-memory index. The server (`server.js`) swaps that index in, compares it with what it has seen before, and notifies followers of new chapters. The library itself is only ever read. ``` ┌───────────── triggers ─────────────┐ @@ -177,7 +177,7 @@ The scan uses asynchronous I/O one file at a time, so it never blocks request ha 2. Otherwise, a chapter id not in `known` is new. It is appended; the list **only grows**, so a temporarily missing folder or an unmounted disk never causes a second round of notifications when it comes back. 3. For every user following the manga: - one in-app notification per new chapter (text `Title · Ch. 7: Thaw`; at most 200 kept per user), - - if the user has a webhook URL, **one** `POST` per manga per scan, with a plain-text body (`New in Moon Garden: Ch. 7 Thaw, Ch. 8 Bloom`), header `Title: MangaParty`, and a 10 s timeout. Failures are logged, not retried. + - if the user has a webhook URL, **one** `POST` per manga per scan, with a plain-text body (`New in Moon Garden: Ch. 7 Thaw, Ch. 8 Bloom`), header `Title: MangaCircus`, and a 10 s timeout. Failures are logged, not retried. 4. Opening a chapter marks its notifications as read. What counts as "new" follows from the chapter id (`book key/chapter key`): diff --git a/docs/library-format.md b/docs/library-format.md index 14495ec..865437a 100644 --- a/docs/library-format.md +++ b/docs/library-format.md @@ -1,6 +1,6 @@ # The file system library -MangaParty has no import database for content. The **library folder is the source of truth**. The app reads it in place (read-only access is enough), and anything that can write files can add manga: a scraper, a sync tool, or a person with a file browser. +MangaCircus has no import database for content. The **library folder is the source of truth**. The app reads it in place (read-only access is enough), and anything that can write files can add manga: a scraper, a sync tool, or a person with a file browser. This document is the format specification. [import.md](import.md) describes how the app reads it, and [scraper-guide.md](scraper-guide.md) is a rule book for tools that write it. diff --git a/docs/scraper-guide.md b/docs/scraper-guide.md index cf24dd3..b0d7e96 100644 --- a/docs/scraper-guide.md +++ b/docs/scraper-guide.md @@ -1,36 +1,36 @@ -# Scraper guide: supplying the MangaParty library +# Scraper guide: supplying the MangaCircus library -**Audience:** an automated agent or program that downloads manga, comics or webtoons from upstream sources and writes them into a MangaParty library folder. +**Audience:** an automated agent or program that downloads manga, comics or webtoons from upstream sources and writes them into a MangaCircus library folder. **Normative words:** MUST, MUST NOT, SHOULD, SHOULD NOT and MAY as in RFC 2119. Rules have ids (`R-…`) so you can cite them in logs and reviews. **Background reading** (optional; this guide is self-contained): - [library-format.md](library-format.md): the format. -- [import.md](import.md): how MangaParty reads it. +- [import.md](import.md): how MangaCircus reads it. --- ## 0. The contract in short - You write **plain folders and image files**. There is no upload API. -- MangaParty scans the library folder every `SCAN_INTERVAL` minutes (default 10), and on demand (§9). +- MangaCircus scans the library folder every `SCAN_INTERVAL` minutes (default 10), and on demand (§9). - The layout is `////`, always three levels. - Names carry meaning: - the manga folder name is the manga's id, - the book/chapter **numbers** in folder names are ids and order, - the chapter folder name is the version id, - page file names carry order and page roles. -- MangaParty notifies followers as soon as a new chapter number becomes visible. So a chapter MUST become visible **complete and in one step**: stage it in a hidden folder, then rename it (§10). -- MangaParty never writes to the library. You own it; MangaParty only reads it. +- MangaCircus notifies followers as soon as a new chapter number becomes visible. So a chapter MUST become visible **complete and in one step**: stage it in a hidden folder, then rename it (§10). +- MangaCircus never writes to the library. You own it; MangaCircus only reads it. ## 1. Inputs you need | Input | Required | Notes | |-------|----------|-------| -| Library root path | yes | The folder MangaParty mounts as `LIBRARY` (`/library` in Docker) | +| Library root path | yes | The folder MangaCircus mounts as `LIBRARY` (`/library` in Docker) | | Upstream manga metadata | yes | Title, reading direction/type, chapter list with numbers | | Per chapter: ordered page images | yes | Plus, where available: which pages are covers, spreads, credits/notes | -| MangaParty URL + an admin account | no | Only to trigger an immediate rescan (§9) | +| MangaCircus URL + an admin account | no | Only to trigger an immediate rescan (§9) | ## 2. Target layout (template) @@ -69,7 +69,7 @@ - `x[ ]` for extra books (specials, art books, one-shot collections) that sit outside the numbering. - Example: `v01 Spring`, `s02 Season 2`, `x01 Specials`. - **R-B2** Pad numbers to 2 digits, or to 3 digits for series that can exceed 99 volumes, and keep the width the same within a manga. Re-padding a book folder later (`v01` → `v001`) is safe: the number, not the text, is the id. -- **R-B3** Book numbers MUST be unique within a manga. MangaParty ignores a second folder with the same number. +- **R-B3** Book numbers MUST be unique within a manga. MangaCircus ignores a second folder with the same number. - **R-B4** A chapter's id includes its book number (`<book>/<chapter>`). Moving a chapter to another book later creates a "new" chapter: progress and votes stay behind, and followers get notified again. Therefore you MUST choose a book strategy per manga that never needs to move chapters: - **Flat (default):** every chapter goes into `v01` (for example `v01 Chapters`). Use it for ongoing series and for sources without reliable volume data. - **Volumes:** chapters go into their volume, **only** if the source gives a final volume number at the time you first write the chapter, and for every chapter. @@ -212,8 +212,8 @@ All are UTF-8 JSON **without** BOM, pretty-printing optional. New content shows up by itself at the next scheduled scan. For immediate pickup, trigger **one** rescan after a batch, not per chapter: ```bash -curl -s -c jar.txt -H 'Content-Type: application/json' -d '{"name":"<admin>","password":"<password>"}' "$MANGAPARTY/api/login" -curl -s -b jar.txt -H 'Content-Type: application/json' -d '{}' "$MANGAPARTY/api/admin/rescan" +curl -s -c jar.txt -H 'Content-Type: application/json' -d '{"name":"<admin>","password":"<password>"}' "$MANGACIRCUS/api/login" +curl -s -b jar.txt -H 'Content-Type: application/json' -d '{}' "$MANGACIRCUS/api/admin/rescan" ``` - The response contains `scan.error` (null on success) and counts. @@ -221,7 +221,7 @@ curl -s -b jar.txt -H 'Content-Type: application/json' -d '{}' "$MANGAPARTY/api/ ## 10. Writing safely -MangaParty can scan at any moment. Everything it can see must always be complete. +MangaCircus can scan at any moment. Everything it can see must always be complete. - **R-W1** All staging paths MUST start with `.` and MUST be on the **same file system** as their final location; only then is `rename` atomic. Put staging folders next to the target: @@ -232,7 +232,7 @@ MangaParty can scan at any moment. Everything it can see must always be complete | new manga | `<library>/.staging-<random>/` | - **R-W2** Publish = one `rename` of the finished staging folder to its final name. Write every image and `chapter.json` before that rename. -- **R-W3** You MUST NOT overwrite image files in place. MangaParty caches image sizes per folder mtime, and an in-place overwrite goes unnoticed. Replace whole version folders instead (R-W4). +- **R-W3** You MUST NOT overwrite image files in place. MangaCircus caches image sizes per folder mtime, and an in-place overwrite goes unnoticed. Replace whole version folders instead (R-W4). - **R-W4** Replacing an existing version (same folder name): 1. stage the new copy, 2. `rename(final → .old-<random>)`, @@ -242,8 +242,8 @@ MangaParty can scan at any moment. Everything it can see must always be complete - **R-W5** Updating a JSON file: write `.<name>.tmp` in the same folder, then `rename` it over the target. - **R-W6** At start-up, delete your own leftover `.staging-*` / `.old-*` folders older than a few hours (from crashed runs). - **R-W7** On Windows, `rename` can fail with `EPERM`/`EBUSY` while another process reads the folder. Retry a few times with a short delay. -- **R-W8** Files MUST be readable by the MangaParty process. In Docker that is uid 1000, so use mode `0644` for files and `0755` for folders. -- **R-W9** You MUST NOT write anything into MangaParty's `DATA` folder (`db.json`, `scan-cache.json`). +- **R-W8** Files MUST be readable by the MangaCircus process. In Docker that is uid 1000, so use mode `0644` for files and `0755` for folders. +- **R-W9** You MUST NOT write anything into MangaCircus's `DATA` folder (`db.json`, `scan-cache.json`). ### 10.1 Reference implementation (Node.js, no dependencies) @@ -323,9 +323,9 @@ after the batch: optionally trigger one rescan - [ ] Publishing is a single rename. - [ ] No secrets in `manga.json`. -## 13. What MangaParty does with your output +## 13. What MangaCircus does with your output -| You do | MangaParty does | +| You do | MangaCircus does | |--------|-----------------| | Publish a manga folder for the first time | Adds it to the library. **No** notifications (first sight) | | Publish a new chapter number in a known manga | Lists it, and notifies followers in the app and by webhook within one scan | @@ -335,7 +335,7 @@ after the batch: optionally trigger one rescan | Move or renumber a chapter | Treats it as a new chapter and notifies again. Old progress and votes are orphaned | | Rename a version folder | Votes for it are lost | | Remove a chapter | It disappears from the list. It is not re-announced if it comes back | -| Write invalid names or JSON | Skips the folder or ignores the JSON, and logs a `[scan]` warning in MangaParty's output | +| Write invalid names or JSON | Skips the folder or ignores the JSON, and logs a `[scan]` warning in MangaCircus's output | ## 14. Worked example diff --git a/package.json b/package.json index 48bcdc0..a403332 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "mangaparty", + "name": "mangacircus", "version": "1.0.0", "private": true, "description": "No-frills self-hosted manga reader. No dependencies.", diff --git a/public/app.css b/public/app.css index 07fffdf..33b5489 100644 --- a/public/app.css +++ b/public/app.css @@ -132,15 +132,15 @@ body.reader { position: fixed; top: 0; left: 0; right: 0; bottom: 0; overflow: h .pg.loading::after { content: ''; position: absolute; left: 50%; top: 50%; width: 38px; height: 38px; margin: -19px 0 0 -19px; border: 3px solid rgba(255, 255, 255, .15); border-top-color: var(--accent); border-radius: 50%; - opacity: 0; animation: mp-spin .8s linear infinite, mp-show 0s .15s forwards; + opacity: 0; animation: mc-spin .8s linear infinite, mc-show 0s .15s forwards; } .pg.failed::after { content: 'This page could not be loaded'; position: absolute; top: 0; left: 0; right: 0; bottom: 0; display: flex; align-items: center; justify-content: center; color: var(--muted); font-size: 14px; text-align: center; } .pg.failed img { visibility: hidden; } -@keyframes mp-spin { to { transform: rotate(360deg); } } -@keyframes mp-show { to { opacity: 1; } } +@keyframes mc-spin { to { transform: rotate(360deg); } } +@keyframes mc-show { to { opacity: 1; } } .loading { color: var(--muted); padding: 40px; text-align: center; } .end { position: absolute; left: 0; right: 0; display: flex; align-items: center; justify-content: center; } #canvas.page .end, #canvas.book .end { top: 0; bottom: 0; } diff --git a/public/common.js b/public/common.js index c1e3199..1749399 100644 --- a/public/common.js +++ b/public/common.js @@ -36,8 +36,8 @@ async function api(path, body) { } const store = { - get(k) { try { return localStorage.getItem('mp.' + k); } catch (e) { return null; } }, - set(k, v) { try { localStorage.setItem('mp.' + k, v); } catch (e) { /* private mode */ } }, + get(k) { try { return localStorage.getItem('mc.' + k); } catch (e) { return null; } }, + set(k, v) { try { localStorage.setItem('mc.' + k, v); } catch (e) { /* private mode */ } }, }; function ago(t) { @@ -79,7 +79,7 @@ function topNav(me) { panel.addEventListener('click', e => e.stopPropagation()); document.addEventListener('click', () => { panel.hidden = true; }); const nav = h('header', { class: 'nav' }, - h('a', { class: 'brand', href: './' }, 'MangaParty'), + h('a', { class: 'brand', href: './' }, 'MangaCircus'), h('div', { class: 'grow' }), h('div', { class: 'bell-wrap' }, bell, panel), h('a', { class: 'btn', href: 'settings.html' }, 'Settings'), diff --git a/public/index.html b/public/index.html index a3ba5c0..5d40ba7 100644 --- a/public/index.html +++ b/public/index.html @@ -3,7 +3,7 @@ <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> -<title>MangaParty +MangaCircus diff --git a/public/login.html b/public/login.html index a99ef6f..9a8ad54 100644 --- a/public/login.html +++ b/public/login.html @@ -3,7 +3,7 @@ -Sign in · MangaParty +Sign in · MangaCircus @@ -13,7 +13,7 @@
-

MangaParty

+

MangaCircus

Sign in

diff --git a/public/manga.html b/public/manga.html index ef15e17..3e83c03 100644 --- a/public/manga.html +++ b/public/manga.html @@ -3,7 +3,7 @@ -MangaParty +MangaCircus diff --git a/public/manga.js b/public/manga.js index 20c618b..f429fcf 100644 --- a/public/manga.js +++ b/public/manga.js @@ -5,7 +5,7 @@ let me, M; try { [me, M] = await Promise.all([api('api/me'), api('api/manga?' + qsa({ m: id }))]); } catch (e) { return showError(e); } topNav(me); - document.title = M.title + ' · MangaParty'; + document.title = M.title + ' · MangaCircus'; const meta = M.meta || {}, main = $('main'); main.textContent = ''; diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest index 00eaa99..ab65bc2 100644 --- a/public/manifest.webmanifest +++ b/public/manifest.webmanifest @@ -1,6 +1,6 @@ { - "name": "MangaParty", - "short_name": "MangaParty", + "name": "MangaCircus", + "short_name": "MangaCircus", "start_url": "./", "scope": "./", "display": "standalone", diff --git a/public/read.html b/public/read.html index f14932e..be26d43 100644 --- a/public/read.html +++ b/public/read.html @@ -6,7 +6,7 @@ -MangaParty +MangaCircus diff --git a/public/settings.html b/public/settings.html index c983b29..2d93b5a 100644 --- a/public/settings.html +++ b/public/settings.html @@ -3,7 +3,7 @@ -Settings · MangaParty +Settings · MangaCircus diff --git a/server.js b/server.js index acdbdc2..9208e0d 100644 --- a/server.js +++ b/server.js @@ -1,6 +1,6 @@ 'use strict'; /* - * MangaParty: a no-frills, self-hosted manga reader. No dependencies, just `node server.js`. + * MangaCircus: a no-frills, self-hosted manga reader. No dependencies, just `node server.js`. * * Environment: * PORT=8080 LIBRARY=./library DATA=./data @@ -116,7 +116,7 @@ function notify(user, m, chapters) { const hook = settingsOf(user).webhook; if (hook) { const body = `New in ${m.title}: ${chapters.map(c => c.label + (c.title ? ' ' + c.title : '')).join(', ')}`; - fetch(hook, { method: 'POST', body, headers: { 'Content-Type': 'text/plain; charset=utf-8', Title: 'MangaParty' }, signal: AbortSignal.timeout(10000) }) + fetch(hook, { method: 'POST', body, headers: { 'Content-Type': 'text/plain; charset=utf-8', Title: 'MangaCircus' }, signal: AbortSignal.timeout(10000) }) .catch(e => console.warn(`[notify] webhook of ${user.name} failed: ${e.message}`)); } } @@ -371,7 +371,7 @@ http.createServer(async (req, res) => { if (!(e instanceof HttpError)) console.error(e); if (!res.headersSent) sendJson(res, e.code || 500, { error: e instanceof HttpError ? e.message : 'Internal error' }); } -}).listen(PORT, () => console.log(`MangaParty on http://localhost:${PORT} (library: ${LIB}, data: ${DATA})`)); +}).listen(PORT, () => console.log(`MangaCircus on http://localhost:${PORT} (library: ${LIB}, data: ${DATA})`)); rescan(); if (SCAN_MINUTES > 0) setInterval(() => rescan(), SCAN_MINUTES * 60000).unref();