Rename the app from MangaParty to MangaCircus
This commit is contained in:
1 parent
8dd776b7f4
commit
cfb2d79103
19 files changed
+49
-49
No files matched your search
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "mangaparty",
|
"name": "mangacircus",
|
||||||
"runtimeExecutable": "node",
|
"runtimeExecutable": "node",
|
||||||
"runtimeArgs": ["server.js"],
|
"runtimeArgs": ["server.js"],
|
||||||
"port": 8080
|
"port": 8080
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
# --- MangaParty runtime data (never commit) ---
|
# --- MangaCircus runtime data (never commit) ---
|
||||||
# user accounts (password hashes), sessions, progress, scan cache
|
# user accounts (password hashes), sessions, progress, scan cache
|
||||||
/data/
|
/data/
|
||||||
# the manga library: demo content is generated by `node tools/make-dummy.js`, a real one is yours alone
|
# the manga library: demo content is generated by `node tools/make-dummy.js`, a real one is yours alone
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# MangaParty
|
# MangaCircus
|
||||||
|
|
||||||
A no-frills, self-hosted manga / comic / webtoon reader.
|
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 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)
|
### Settings (environment variables)
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -1,15 +1,15 @@
|
|||||||
services:
|
services:
|
||||||
mangaparty:
|
mangacircus:
|
||||||
build: .
|
build: .
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
volumes:
|
volumes:
|
||||||
- ./library:/library:ro # your manga folders (read-only is fine)
|
- ./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:
|
environment:
|
||||||
SIGNUP: closed # "open" lets anyone create an account
|
SIGNUP: closed # "open" lets anyone create an account
|
||||||
SCAN_INTERVAL: 10 # minutes between automatic rescans (0 = off)
|
SCAN_INTERVAL: 10 # minutes between automatic rescans (0 = off)
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
mangaparty-data:
|
mangacircus-data:
|
||||||
@@ -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:
|
Related documents:
|
||||||
- [library-format.md](library-format.md): the folder format.
|
- [library-format.md](library-format.md): the folder format.
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
# How the import works
|
# 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 ─────────────┐
|
┌───────────── 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.
|
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:
|
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),
|
- 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.
|
4. Opening a chapter marks its notifications as read.
|
||||||
|
|
||||||
What counts as "new" follows from the chapter id (`book key/chapter key`):
|
What counts as "new" follows from the chapter id (`book key/chapter key`):
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# The file system library
|
# 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.
|
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
@@ -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.
|
**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):
|
**Background reading** (optional; this guide is self-contained):
|
||||||
- [library-format.md](library-format.md): the format.
|
- [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
|
## 0. The contract in short
|
||||||
|
|
||||||
- You write **plain folders and image files**. There is no upload API.
|
- 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.
|
- The layout is `<library>/<Manga>/<Book>/<Chapter>/<pages>`, always three levels.
|
||||||
- Names carry meaning:
|
- Names carry meaning:
|
||||||
- the manga folder name is the manga's id,
|
- the manga folder name is the manga's id,
|
||||||
- the book/chapter **numbers** in folder names are ids and order,
|
- the book/chapter **numbers** in folder names are ids and order,
|
||||||
- the chapter folder name is the version id,
|
- the chapter folder name is the version id,
|
||||||
- page file names carry order and page roles.
|
- 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).
|
- 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).
|
||||||
- MangaParty never writes to the library. You own it; MangaParty only reads it.
|
- MangaCircus never writes to the library. You own it; MangaCircus only reads it.
|
||||||
|
|
||||||
## 1. Inputs you need
|
## 1. Inputs you need
|
||||||
|
|
||||||
| Input | Required | Notes |
|
| 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 |
|
| 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 |
|
| 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)
|
## 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.
|
- `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`.
|
- 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-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:
|
- **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.
|
- **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.
|
- **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:
|
New content shows up by itself at the next scheduled scan. For immediate pickup, trigger **one** rescan after a batch, not per chapter:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -s -c jar.txt -H 'Content-Type: application/json' -d '{"name":"<admin>","password":"<password>"}' "$MANGAPARTY/api/login"
|
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 '{}' "$MANGAPARTY/api/admin/rescan"
|
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.
|
- 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
|
## 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:
|
- **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>/` |
|
| 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-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):
|
- **R-W4** Replacing an existing version (same folder name):
|
||||||
1. stage the new copy,
|
1. stage the new copy,
|
||||||
2. `rename(final → .old-<random>)`,
|
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-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-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-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-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 MangaParty's `DATA` folder (`db.json`, `scan-cache.json`).
|
- **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)
|
### 10.1 Reference implementation (Node.js, no dependencies)
|
||||||
|
|
||||||
@@ -323,9 +323,9 @@ after the batch: optionally trigger one rescan
|
|||||||
- [ ] Publishing is a single rename.
|
- [ ] Publishing is a single rename.
|
||||||
- [ ] No secrets in `manga.json`.
|
- [ ] 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 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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
|
## 14. Worked example
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "mangaparty",
|
"name": "mangacircus",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "No-frills self-hosted manga reader. No dependencies.",
|
"description": "No-frills self-hosted manga reader. No dependencies.",
|
||||||
|
|||||||
+3
-3
@@ -132,15 +132,15 @@ body.reader { position: fixed; top: 0; left: 0; right: 0; bottom: 0; overflow: h
|
|||||||
.pg.loading::after {
|
.pg.loading::after {
|
||||||
content: ''; position: absolute; left: 50%; top: 50%; width: 38px; height: 38px; margin: -19px 0 0 -19px;
|
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%;
|
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 {
|
.pg.failed::after {
|
||||||
content: 'This page could not be loaded'; position: absolute; top: 0; left: 0; right: 0; bottom: 0;
|
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;
|
display: flex; align-items: center; justify-content: center; color: var(--muted); font-size: 14px; text-align: center;
|
||||||
}
|
}
|
||||||
.pg.failed img { visibility: hidden; }
|
.pg.failed img { visibility: hidden; }
|
||||||
@keyframes mp-spin { to { transform: rotate(360deg); } }
|
@keyframes mc-spin { to { transform: rotate(360deg); } }
|
||||||
@keyframes mp-show { to { opacity: 1; } }
|
@keyframes mc-show { to { opacity: 1; } }
|
||||||
.loading { color: var(--muted); padding: 40px; text-align: center; }
|
.loading { color: var(--muted); padding: 40px; text-align: center; }
|
||||||
.end { position: absolute; left: 0; right: 0; display: flex; align-items: center; justify-content: 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; }
|
#canvas.page .end, #canvas.book .end { top: 0; bottom: 0; }
|
||||||
|
|||||||
+3
-3
@@ -36,8 +36,8 @@ async function api(path, body) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const store = {
|
const store = {
|
||||||
get(k) { try { return localStorage.getItem('mp.' + k); } catch (e) { return null; } },
|
get(k) { try { return localStorage.getItem('mc.' + k); } catch (e) { return null; } },
|
||||||
set(k, v) { try { localStorage.setItem('mp.' + k, v); } catch (e) { /* private mode */ } },
|
set(k, v) { try { localStorage.setItem('mc.' + k, v); } catch (e) { /* private mode */ } },
|
||||||
};
|
};
|
||||||
|
|
||||||
function ago(t) {
|
function ago(t) {
|
||||||
@@ -79,7 +79,7 @@ function topNav(me) {
|
|||||||
panel.addEventListener('click', e => e.stopPropagation());
|
panel.addEventListener('click', e => e.stopPropagation());
|
||||||
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: './' }, 'MangaParty'),
|
h('a', { class: 'brand', href: './' }, 'MangaCircus'),
|
||||||
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'),
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
<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="stylesheet" href="app.css">
|
||||||
<link rel="icon" href="icon.svg">
|
<link rel="icon" href="icon.svg">
|
||||||
<link rel="manifest" href="manifest.webmanifest">
|
<link rel="manifest" href="manifest.webmanifest">
|
||||||
|
|||||||
+2
-2
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
<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="stylesheet" href="app.css">
|
||||||
<link rel="icon" href="icon.svg">
|
<link rel="icon" href="icon.svg">
|
||||||
<link rel="manifest" href="manifest.webmanifest">
|
<link rel="manifest" href="manifest.webmanifest">
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main class="login">
|
<main class="login">
|
||||||
<h1>MangaParty</h1>
|
<h1>MangaCircus</h1>
|
||||||
<form id="form" class="card">
|
<form id="form" class="card">
|
||||||
<h2 id="heading">Sign in</h2>
|
<h2 id="heading">Sign in</h2>
|
||||||
<label class="field"><span>User name</span><input id="name" autocomplete="username" autocapitalize="none" required></label>
|
<label class="field"><span>User name</span><input id="name" autocomplete="username" autocapitalize="none" required></label>
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
<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="stylesheet" href="app.css">
|
||||||
<link rel="icon" href="icon.svg">
|
<link rel="icon" href="icon.svg">
|
||||||
<link rel="manifest" href="manifest.webmanifest">
|
<link rel="manifest" href="manifest.webmanifest">
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
let me, M;
|
let me, M;
|
||||||
try { [me, M] = await Promise.all([api('api/me'), api('api/manga?' + qsa({ m: id }))]); } catch (e) { return showError(e); }
|
try { [me, M] = await Promise.all([api('api/me'), api('api/manga?' + qsa({ m: id }))]); } catch (e) { return showError(e); }
|
||||||
topNav(me);
|
topNav(me);
|
||||||
document.title = M.title + ' · MangaParty';
|
document.title = M.title + ' · MangaCircus';
|
||||||
const meta = M.meta || {}, main = $('main');
|
const meta = M.meta || {}, main = $('main');
|
||||||
main.textContent = '';
|
main.textContent = '';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "MangaParty",
|
"name": "MangaCircus",
|
||||||
"short_name": "MangaParty",
|
"short_name": "MangaCircus",
|
||||||
"start_url": "./",
|
"start_url": "./",
|
||||||
"scope": "./",
|
"scope": "./",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-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">
|
<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="stylesheet" href="app.css">
|
||||||
<link rel="icon" href="icon.svg">
|
<link rel="icon" href="icon.svg">
|
||||||
<link rel="manifest" href="manifest.webmanifest">
|
<link rel="manifest" href="manifest.webmanifest">
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
<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="stylesheet" href="app.css">
|
||||||
<link rel="icon" href="icon.svg">
|
<link rel="icon" href="icon.svg">
|
||||||
<link rel="manifest" href="manifest.webmanifest">
|
<link rel="manifest" href="manifest.webmanifest">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use strict';
|
'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:
|
* Environment:
|
||||||
* PORT=8080 LIBRARY=./library DATA=./data
|
* PORT=8080 LIBRARY=./library DATA=./data
|
||||||
@@ -116,7 +116,7 @@ function notify(user, m, chapters) {
|
|||||||
const hook = settingsOf(user).webhook;
|
const hook = settingsOf(user).webhook;
|
||||||
if (hook) {
|
if (hook) {
|
||||||
const body = `New in ${m.title}: ${chapters.map(c => c.label + (c.title ? ' ' + c.title : '')).join(', ')}`;
|
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}`));
|
.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 (!(e instanceof HttpError)) console.error(e);
|
||||||
if (!res.headersSent) sendJson(res, e.code || 500, { error: e instanceof HttpError ? e.message : 'Internal error' });
|
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();
|
rescan();
|
||||||
if (SCAN_MINUTES > 0) setInterval(() => rescan(), SCAN_MINUTES * 60000).unref();
|
if (SCAN_MINUTES > 0) setInterval(() => rescan(), SCAN_MINUTES * 60000).unref();
|
||||||
Reference in new issue
Block a user