rebrand: MangaBM → BookmarkManager, add novel library support #15
+3
-2
@@ -5,8 +5,9 @@
|
|||||||
API_TOKEN=changeme-generate-a-long-random-token
|
API_TOKEN=changeme-generate-a-long-random-token
|
||||||
|
|
||||||
# Comma-separated origins allowed to call the API (CORS). Both Asura domains
|
# Comma-separated origins allowed to call the API (CORS). Both Asura domains
|
||||||
# plus Demonic, Comix, and Kagane. Add/remove as the sites' hostnames change.
|
# plus Demonic, Comix, Kagane, and the two novel sites. Add/remove as the
|
||||||
ALLOWED_ORIGINS=https://asuracomic.net,https://asurascans.com,https://demonicscans.org,https://comix.to,https://kagane.to
|
# 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
|
||||||
|
|
||||||
# --- Prod override (Traefik) only ---
|
# --- Prod override (Traefik) only ---
|
||||||
# Subdomain Traefik routes to this service (required by the prod override).
|
# Subdomain Traefik routes to this service (required by the prod override).
|
||||||
|
|||||||
@@ -470,3 +470,40 @@ func TestUserscriptServedWithWebUIDisabled(t *testing.T) {
|
|||||||
t.Fatalf("status = %d, want 200", rr.Code)
|
t.Fatalf("status = %d, want 200", rr.Code)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Both scripts are served from the same handler on the same token, outside the
|
||||||
|
// WEB_PASSWORD gate — a wrong token is a 404, never a 401.
|
||||||
|
func TestNovelUserscriptServed(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
novelPath := filepath.Join(dir, "novel-bookmark.user.js")
|
||||||
|
if err := os.WriteFile(novelPath, []byte("// novel\n"), 0o644); err != nil {
|
||||||
|
t.Fatalf("write script: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := store.Open(filepath.Join(dir, "test.db"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("store.Open: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { s.Close() })
|
||||||
|
|
||||||
|
cfg := testConfig()
|
||||||
|
cfg.NovelUserscriptPath = novelPath
|
||||||
|
srv := newRouter(s, cfg)
|
||||||
|
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
srv.ServeHTTP(rr, httptest.NewRequest(http.MethodGet,
|
||||||
|
"/u/"+testToken+"/novel-bookmark.user.js", nil))
|
||||||
|
if rr.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want 200", rr.Code)
|
||||||
|
}
|
||||||
|
if ct := rr.Header().Get("Content-Type"); !strings.HasPrefix(ct, "text/javascript") {
|
||||||
|
t.Fatalf("Content-Type = %q, want text/javascript", ct)
|
||||||
|
}
|
||||||
|
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
srv.ServeHTTP(rr, httptest.NewRequest(http.MethodGet,
|
||||||
|
"/u/wrong-token/novel-bookmark.user.js", nil))
|
||||||
|
if rr.Code != http.StatusNotFound {
|
||||||
|
t.Fatalf("wrong token status = %d, want 404", rr.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+12
-6
@@ -31,6 +31,10 @@ type Config struct {
|
|||||||
// UserscriptPath is the file served at /u/{token}/manga-bookmark.user.js.
|
// UserscriptPath is the file served at /u/{token}/manga-bookmark.user.js.
|
||||||
// Supplied by a bindmount so the script can be edited without a rebuild.
|
// Supplied by a bindmount so the script can be edited without a rebuild.
|
||||||
UserscriptPath string
|
UserscriptPath string
|
||||||
|
// NovelUserscriptPath is the file served at
|
||||||
|
// /u/{token}/novel-bookmark.user.js. Same bindmount, second script: the
|
||||||
|
// two libraries are separate installs.
|
||||||
|
NovelUserscriptPath string
|
||||||
// LatestPoll configures the background latest-chapter fetcher.
|
// LatestPoll configures the background latest-chapter fetcher.
|
||||||
LatestPoll LatestPoll
|
LatestPoll LatestPoll
|
||||||
}
|
}
|
||||||
@@ -138,12 +142,13 @@ func loadLatestPoll() LatestPoll {
|
|||||||
|
|
||||||
func loadConfig() Config {
|
func loadConfig() Config {
|
||||||
c := Config{
|
c := Config{
|
||||||
Token: os.Getenv("API_TOKEN"),
|
Token: os.Getenv("API_TOKEN"),
|
||||||
DBPath: envOr("DB_PATH", "/data/bookmarks.db"),
|
DBPath: envOr("DB_PATH", "/data/bookmarks.db"),
|
||||||
Port: envOr("PORT", "8080"),
|
Port: envOr("PORT", "8080"),
|
||||||
WebPassword: os.Getenv("WEB_PASSWORD"),
|
WebPassword: os.Getenv("WEB_PASSWORD"),
|
||||||
UserscriptPath: envOr("USERSCRIPT_PATH", "/userscript/manga-bookmark.user.js"),
|
UserscriptPath: envOr("USERSCRIPT_PATH", "/userscript/manga-bookmark.user.js"),
|
||||||
LatestPoll: loadLatestPoll(),
|
NovelUserscriptPath: envOr("NOVEL_USERSCRIPT_PATH", "/userscript/novel-bookmark.user.js"),
|
||||||
|
LatestPoll: loadLatestPoll(),
|
||||||
}
|
}
|
||||||
for _, o := range strings.Split(os.Getenv("ALLOWED_ORIGINS"), ",") {
|
for _, o := range strings.Split(os.Getenv("ALLOWED_ORIGINS"), ",") {
|
||||||
if o = strings.TrimSpace(o); o != "" {
|
if o = strings.TrimSpace(o); o != "" {
|
||||||
@@ -164,6 +169,7 @@ func newRouter(s *store.Store, cfg Config) http.Handler {
|
|||||||
// outside the WEB_PASSWORD gate (the script must be installable either
|
// outside the WEB_PASSWORD gate (the script must be installable either
|
||||||
// way). The path segment carries the token instead.
|
// way). The path segment carries the token instead.
|
||||||
mux.HandleFunc("GET /u/{token}/manga-bookmark.user.js", userscript.Handler(cfg.Token, cfg.UserscriptPath))
|
mux.HandleFunc("GET /u/{token}/manga-bookmark.user.js", userscript.Handler(cfg.Token, cfg.UserscriptPath))
|
||||||
|
mux.HandleFunc("GET /u/{token}/novel-bookmark.user.js", userscript.Handler(cfg.Token, cfg.NovelUserscriptPath))
|
||||||
|
|
||||||
h := &api.Handler{Store: s}
|
h := &api.Handler{Store: s}
|
||||||
protected := http.NewServeMux()
|
protected := http.NewServeMux()
|
||||||
|
|||||||
+4
-1
@@ -15,13 +15,16 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
# API_TOKEN is required — compose refuses to start without it.
|
# API_TOKEN is required — compose refuses to start without it.
|
||||||
API_TOKEN: ${API_TOKEN:?set API_TOKEN in .env}
|
API_TOKEN: ${API_TOKEN:?set API_TOKEN in .env}
|
||||||
ALLOWED_ORIGINS: ${ALLOWED_ORIGINS:-https://asuracomic.net,https://asurascans.com,https://demonicscans.org,https://comix.to,https://kagane.to}
|
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}
|
||||||
DB_PATH: /data/bookmarks.db
|
DB_PATH: /data/bookmarks.db
|
||||||
PORT: "8080"
|
PORT: "8080"
|
||||||
# Gates the browser UI. Unset means the web routes are not served at all.
|
# Gates the browser UI. Unset means the web routes are not served at all.
|
||||||
WEB_PASSWORD: ${WEB_PASSWORD:-}
|
WEB_PASSWORD: ${WEB_PASSWORD:-}
|
||||||
# Path inside the container; matches the bindmount above.
|
# Path inside the container; matches the bindmount above.
|
||||||
USERSCRIPT_PATH: ${USERSCRIPT_PATH:-/userscript/manga-bookmark.user.js}
|
USERSCRIPT_PATH: ${USERSCRIPT_PATH:-/userscript/manga-bookmark.user.js}
|
||||||
|
# Second script from the same bindmount; the novel library is a separate
|
||||||
|
# Violentmonkey install.
|
||||||
|
NOVEL_USERSCRIPT_PATH: ${NOVEL_USERSCRIPT_PATH:-/userscript/novel-bookmark.user.js}
|
||||||
# Latest-chapter poller. LATEST_CHAPTER_POLL_ENABLED=0 in .env is the kill
|
# Latest-chapter poller. LATEST_CHAPTER_POLL_ENABLED=0 in .env is the kill
|
||||||
# switch; it only takes effect because these are listed here.
|
# switch; it only takes effect because these are listed here.
|
||||||
LATEST_CHAPTER_POLL_ENABLED: ${LATEST_CHAPTER_POLL_ENABLED:-1}
|
LATEST_CHAPTER_POLL_ENABLED: ${LATEST_CHAPTER_POLL_ENABLED:-1}
|
||||||
|
|||||||
Reference in New Issue
Block a user