Rename the app from MangaParty to MangaCircus

This commit is contained in:
Maashkinoozhe committed 2026-10-01 09:37:52 +02:00
1 parent 8dd776b7f4
commit cfb2d79103
19 files changed
+49 -49

No files matched your search

+1 -1
View File
@@ -2,7 +2,7 @@
"version": "0.0.1",
"configurations": [
{
"name": "mangaparty",
"name": "mangacircus",
"runtimeExecutable": "node",
"runtimeArgs": ["server.js"],
"port": 8080
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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)
+3 -3
View File
@@ -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:
+2 -2
View File
@@ -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.
+2 -2
View File
@@ -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`):
+1 -1
View File
@@ -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.
+18 -18
View File
@@ -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 `<library>/<Manga>/<Book>/<Chapter>/<pages>`, 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<NN>[ <title>]` 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
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "mangaparty",
"name": "mangacircus",
"version": "1.0.0",
"private": true,
"description": "No-frills self-hosted manga reader. No dependencies.",
+3 -3
View File
@@ -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; }
+3 -3
View File
@@ -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'),
+1 -1
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>MangaParty</title>
<title>MangaCircus</title>
<link rel="stylesheet" href="app.css">
<link rel="icon" href="icon.svg">
<link rel="manifest" href="manifest.webmanifest">
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Sign in · MangaParty</title>
<title>Sign in · MangaCircus</title>
<link rel="stylesheet" href="app.css">
<link rel="icon" href="icon.svg">
<link rel="manifest" href="manifest.webmanifest">
@@ -13,7 +13,7 @@
</head>
<body>
<main class="login">
<h1>MangaParty</h1>
<h1>MangaCircus</h1>
<form id="form" class="card">
<h2 id="heading">Sign in</h2>
<label class="field"><span>User name</span><input id="name" autocomplete="username" autocapitalize="none" required></label>
+1 -1
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>MangaParty</title>
<title>MangaCircus</title>
<link rel="stylesheet" href="app.css">
<link rel="icon" href="icon.svg">
<link rel="manifest" href="manifest.webmanifest">
+1 -1
View File
@@ -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 = '';
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "MangaParty",
"short_name": "MangaParty",
"name": "MangaCircus",
"short_name": "MangaCircus",
"start_url": "./",
"scope": "./",
"display": "standalone",
+1 -1
View File
@@ -6,7 +6,7 @@
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<title>MangaParty</title>
<title>MangaCircus</title>
<link rel="stylesheet" href="app.css">
<link rel="icon" href="icon.svg">
<link rel="manifest" href="manifest.webmanifest">
+1 -1
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Settings · MangaParty</title>
<title>Settings · MangaCircus</title>
<link rel="stylesheet" href="app.css">
<link rel="icon" href="icon.svg">
<link rel="manifest" href="manifest.webmanifest">
+3 -3
View File
@@ -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();