diff --git a/.env.example b/.env.example index 0d8e37e..487548b 100644 --- a/.env.example +++ b/.env.example @@ -15,7 +15,7 @@ OWNER_DISCORD_ID=changeme-your-discord-user-id # Comma-separated origins allowed to call the API (CORS). Both Asura domains # plus Demonic, Comix, Kagane, and the two novel sites. Add/remove as the # sites' hostnames change. -ALLOWED_ORIGINS=https://asuracomic.net,https://asurascans.com,https://demonicscans.org,https://comix.to,https://kagane.to,https://novelfull.com,https://lightnovelworld.net +ALLOWED_ORIGINS=https://asurascans.com,https://demonicscans.org,https://comix.to,https://kagane.to,https://novelfull.com,https://lightnovelworld.net # Password for the bundled Postgres container, and therefore half of the # DATABASE_URL compose builds for the backend. Generate one: diff --git a/AGENTS.md b/AGENTS.md index 24b6d7d..3d3bdba 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,7 @@ Guidance for OpenCode (and Claude Code) working in this repo. Read-progress tracker for two libraries — manga and novels — behind one self-hosted Go backend. Two separate Violentmonkey userscripts inject on-page UI (floating button + slide-in panel) and sync progress, so bookmarks unify across sites and devices: -- `manga-bookmark.user.js` — **asurascans.com** (current domain; asuracomic.net 301s here), **demonicscans.org**, **comix.to**, **kagane.to**. +- `manga-bookmark.user.js` — **asurascans.com** (asuracomic.net is dropped: its deep links 301 to the asurascans.com root, discarding the path), **demonicscans.org**, **comix.to**, **kagane.to**. - `novel-bookmark.user.js` — **novelfull.com**, **lightnovelworld.net**. One backend, one `bookmarks` table: a `kind` column (`manga`|`novel`) splits the libraries and the web UI switches between them. Rows are keyed `:`. diff --git a/DEPLOY.md b/DEPLOY.md index 2d57b29..6321d0c 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -43,7 +43,7 @@ TOKEN_KEY= OWNER_DISCORD_ID= # CORS allowlist — leave as-is unless a site changes hostname. -ALLOWED_ORIGINS=https://asuracomic.net,https://asurascans.com,https://demonicscans.org,https://comix.to,https://kagane.to +ALLOWED_ORIGINS=https://asurascans.com,https://demonicscans.org,https://comix.to,https://kagane.to,https://novelfull.com,https://lightnovelworld.net # Required — password for the bundled Postgres container. Compose builds the # backend's DATABASE_URL out of it and has no fallback for either. diff --git a/README.md b/README.md index adcdee1..5dc0b35 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Manga Bookmark -Track manga read-progress on **asurascans.com** (a.k.a. asuracomic.net), +Track manga read-progress on **asurascans.com**, **demonicscans.org**, **comix.to**, and **kagane.to** from a phone (Bromite / mobile Chromium), synced to a self-hosted Go backend so bookmarks unify across all four sites and all devices. @@ -274,11 +274,12 @@ the backend acquires, stores and serves every Cover from its own origin | **Kagane** (`kagane.to`) | `/series/` | `/series//reader/` | `` | Notes: -- **`asuracomic.net` deep links are dead (re-checked 2026-07-25).** They 301 to - the `asurascans.com` **root**, discarding the path, at the edge — before the - userscript gets a document — so nothing client-side can rescue them. Reach - series through `asurascans.com`. The host stays matched in case the redirect - starts preserving paths again. +- **`asuracomic.net` is no longer matched (deep links dead, re-checked + 2026-07-25).** They 301 to the `asurascans.com` **root**, discarding the path, + at the edge — before the userscript gets a document — so nothing client-side + can rescue them. The backend rejects stored addresses on that host too, since + the poller pins each Site to one hostname. Reach series through + `asurascans.com`. - Asura `og:title` carries a `Chapter N - Read Online \| Asura Scans` suffix that the adapter strips; Demonic chapter `og:title` is ` Chapter N`. - Demonic's `<slug>` is identical on `/manga/…` and the canonical `/title/…` diff --git a/backend/api_test.go b/backend/api_test.go index 1d87b25..44880d4 100644 --- a/backend/api_test.go +++ b/backend/api_test.go @@ -33,7 +33,7 @@ const testCoverBaseURL = "https://bookmarks.test" func testConfig() Config { return Config{ TokenKey: testTokenKey, - AllowedOrigins: []string{"https://asuracomic.net", "https://demonicscans.org"}, + AllowedOrigins: []string{"https://asurascans.com", "https://demonicscans.org"}, Port: "8080", } } @@ -169,7 +169,7 @@ func TestAuthAccepted(t *testing.T) { func TestCORSPreflight(t *testing.T) { srv := newTestServer(t) req := httptest.NewRequest(http.MethodOptions, "/bookmarks/asura:foo-1", nil) - req.Header.Set("Origin", "https://asuracomic.net") + req.Header.Set("Origin", "https://asurascans.com") req.Header.Set("Access-Control-Request-Method", "PUT") rr := httptest.NewRecorder() srv.ServeHTTP(rr, req) @@ -177,7 +177,7 @@ func TestCORSPreflight(t *testing.T) { if rr.Code != http.StatusNoContent { t.Fatalf("preflight status = %d, want 204", rr.Code) } - if got := rr.Header().Get("Access-Control-Allow-Origin"); got != "https://asuracomic.net" { + if got := rr.Header().Get("Access-Control-Allow-Origin"); got != "https://asurascans.com" { t.Fatalf("Allow-Origin = %q, want reflected origin", got) } if got := rr.Header().Get("Access-Control-Allow-Methods"); got == "" { @@ -204,11 +204,11 @@ func TestBookmarkRoundTrip(t *testing.T) { key := "asura:solo-leveling-123" in := store.Bookmark{ Title: "Solo Leveling", - SeriesURL: "https://asuracomic.net/series/solo-leveling-123", - Cover: "https://asuracomic.net/cover.jpg", + SeriesURL: "https://asurascans.com/series/solo-leveling-123", + Cover: "https://asurascans.com/cover.jpg", LastChapter: "Chapter 10", LastChapterNum: 10, - LastChapterURL: "https://asuracomic.net/series/solo-leveling-123/chapter/10", + LastChapterURL: "https://asurascans.com/series/solo-leveling-123/chapter/10", } body, _ := json.Marshal(in) diff --git a/docker-compose.yml b/docker-compose.yml index a6f8ea2..3e7ef6d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,7 +25,7 @@ services: # Owner's Discord user ID — required. Seeds the owner Reader (the # administrator); every other Reader registers on their first login. OWNER_DISCORD_ID: ${OWNER_DISCORD_ID:?set OWNER_DISCORD_ID in .env} - ALLOWED_ORIGINS: ${ALLOWED_ORIGINS:-https://asuracomic.net,https://asurascans.com,https://demonicscans.org,https://comix.to,https://kagane.to,https://novelfull.com,https://lightnovelworld.net} + ALLOWED_ORIGINS: ${ALLOWED_ORIGINS:-https://asurascans.com,https://demonicscans.org,https://comix.to,https://kagane.to,https://novelfull.com,https://lightnovelworld.net} # The bookmarks database. Host is the compose service name; the password # comes from .env so it is never committed. DATABASE_URL: ${DATABASE_URL:-postgres://bookmarks:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}@postgres:5432/bookmarks?sslmode=disable} diff --git a/userscript/manga-bookmark.user.js b/userscript/manga-bookmark.user.js index 4643fd9..257133f 100644 --- a/userscript/manga-bookmark.user.js +++ b/userscript/manga-bookmark.user.js @@ -6,7 +6,6 @@ // @author you // @downloadURL https://bookmark-api.violetcrown.my.id/u/__API_TOKEN__/manga-bookmark.user.js // @updateURL https://bookmark-api.violetcrown.my.id/u/__API_TOKEN__/manga-bookmark.user.js -// @match https://asuracomic.net/* // @match https://asurascans.com/* // @match https://demonicscans.org/* // @match https://comix.to/* @@ -112,12 +111,10 @@ const asura = { site: "asura", - // asuracomic.net deep links 301 to the asurascans.com *root*, dropping the - // path, and that happens at the edge before this script gets a document — - // so those URLs cannot be handled here at all (checked 2026-07-25). It stays - // matched in case the redirect starts preserving paths again; until then, - // reach series through asurascans.com. - matches: (loc) => /(^|\.)asurascans\.com$|(^|\.)asuracomic\.net$/.test(loc.hostname), + // asuracomic.net is not matched: its deep links 301 to the asurascans.com + // *root* at the edge, discarding the path, so this script never sees a + // series document there (re-checked 2026-07-25). + matches: (loc) => /(^|\.)asurascans\.com$/.test(loc.hostname), detect(loc) { const path = loc.pathname; // /comics/<slug-hash>/chapter/<n> diff --git a/userscript/test/logic.test.js b/userscript/test/logic.test.js index f095de3..07c221f 100644 --- a/userscript/test/logic.test.js +++ b/userscript/test/logic.test.js @@ -120,6 +120,14 @@ test("asura.detect returns other for non-series paths", () => { assert.equal(asura.detect(loc("https://asurascans.com/bookmarks")).type, "other"); }); +test("asura.matches accepts only asurascans.com", () => { + assert.equal(asura.matches({ hostname: "asurascans.com" }), true); + assert.equal(asura.matches({ hostname: "www.asurascans.com" }), true); + // Dead domain: deep links 301 to the asurascans.com root, discarding the path. + assert.equal(asura.matches({ hostname: "asuracomic.net" }), false); + assert.equal(asura.matches({ hostname: "asurascans.com.evil.example" }), false); +}); + test("asura.latestChapterFromAnchors takes the highest and skips the First Chapter shortcut", () => { const best = asura.latestChapterFromAnchors([ { href: "/comics/solo-leveling-059befe1/chapter/1", text: "Chapter 1" },