50 KiB
50 KiB
Develop Changelog
2026-04-15 (1)
- Reader: font size control in reading settings
- New "Font size" slider (80–150%, default 105%) in the settings drawer, between "Content width" and "Text colour"
- Applies via CSS custom property
--reader-font-sizeon#chapter-content - Persisted per-device in
localStorageasreader-font-size— iPad and desktop each remember their own preference
2026-04-13 (1)
- Edit metadata: comma-separated tag input fix
PillInput._addinbook.jsnow splits the incoming value on commas before adding — each trimmed, non-empty, non-duplicate part is pushed individually- Applies to genres, subgenres and plain tags; pasting e.g.
Fiction, Thriller, Adventureadds 3 separate pills instead of 1
Released as v0.1.11 on 2026-04-13
2026-04-12 (2)
- Series navigation in the reader
- New
GET /api/series-nav/{filename}endpoint: returns{prev, next}with{filename, title, index, suffix}for adjacent books in the same series, ordered byseries_index ASC, series_suffix ASC; returns{prev: null, next: null}for books without a series - Reader header now shows prev/next series buttons (skip-to-start/skip-to-end icons); hidden for books without a series, visible once
loadSeriesNav()resolves - Hovering a series button shows a tooltip:
#<index><suffix> <title>(e.g.#4 Batman: Year One) markRead()in the reader redirects to/library/read/{next.filename}when a next volume exists, so reading continues without leaving the reader; falls back to the book detail page when the series is complete or the book has no series
- New
2026-04-12 (1)
- Comics: series_volume support for annual series (issue numbers restart each year)
- New
series_volume VARCHAR(20)column onlibrary(migrationmigrate_series_volume); default'' - Stored in EPUB OPF as
<meta name="novela:series_volume" content="…"/>; read back byscan_epub upsert_bookinserts/preservesseries_volumewith the same COALESCE strategy asseries_suffixlist_library_jsonORDER BY now:publisher → author → series → series_volume → series_index → titlePATCH /library/book/{filename}readsseries_volumefrom request body; persists to DB and OPF for both file-based and DB-stored books- Book detail page: displays
(year)after series name whenseries_volumeis set (e.g.Donald Duck (1982) [15]); edit panel has a new "Year/Volume" input field book.js: "Auto" button next to the Title field generatesSeries (Year/Vol) #Numberfrom the current series fields
- New
- Bulk importer:
series_volumesupport- New
%series_volume%placeholder token (orange) for filename pattern parsing - New "Year/Vol." shared metadata field (applies to all files; overridden by per-row value or pattern)
- Preview table has a new "Yr/Vol" column
- New "Auto-generate titles from series info" checkbox: when enabled, rows without a parsed title get title
Series (Year/Vol) #Number - Skip checkbox now always visible for every row (previously only shown when duplicates were detected); any file can be manually excluded before import; skipped rows are dimmed; stats bar shows "X skipped"
POST /api/bulk-check-duplicatesextended: now also checks(series, series_index, author)as fallback — detects duplicates even when the title format has changed; items must includeseriesfield
- New
- Library front-end: sorting within a series now respects
series_volumebeforeseries_indexgroupBySeries,renderAuthorDetail,renderPublisherDetailall sort byseries_volume → series_index → series_suffixgetSeriesSlotsrefactored: when any book in the series hasseries_volumeset, gap-detection runs per volume (year) independently — prevents#5 (1982)and#5 (1983)from colliding in the same slot- Slot index label shows
(year) #indexfor annual series (e.g.(1982) #5); unchanged for regular series
Released as v0.1.10 on 2026-04-12
2026-04-08 (13)
- Library: archive series in one action
- "Archive series" / "Unarchive series" button in the series detail view (
#series/{name}) - New endpoint
POST /library/archive-series— setsarchivedfor all books in the series via a single SQL UPDATE; body:{"series": "…", "archive": true|false}; returns{ok, archived, count} - Button label reflects current state: "Archive series" when any book is active, "Unarchive series" when all are archived
- After the call,
allBooksis updated in place and sidebar counters are recalculated without a full page reload
- "Archive series" / "Unarchive series" button in the series detail view (
2026-04-08 (12)
- TedLouisScraper: title extraction fix
<h2 class="story-page-title">also contains a "Back" button (<a class="btn">) and the author byline (<span class="story-author-by-line">)- Fix: only direct
NavigableStringchildren of the h2 are used as the title, so link and span text is skipped
2026-04-08 (11)
- New scraper:
TedLouisScraper(scrapers/tedlouis.py) fortedlouis.com- Matches all
tedlouis.comURLs; no login required - Entry point is the story index page (e.g.
?t=CWYSqpOryu2rQmT1); raises an error when a chapter URL is used as entry point - Title from direct text nodes of
<h2 class="story-page-title"> - Author from
<span class="story-author-by-line"> <a> - Status from
<span class="story-status-text">(strips "Status: " prefix) - Updated date from
<span class="story-last-updated">→YYYY-MM-DD - Chapters from all
<ul class="story-index-list">elements (three columns); relative?t=TOKENlinks resolved to absolute URLs fetch_chapter(): content from<div id="chapter">; removes story title, chapter title and copyright blocks
- Matches all
2026-04-08 (10)
- Settings: break image upload added
- New "Break image" card on the settings page: upload a PNG/JPG/WebP as the scene break image
- Stored in the imagestore (sha256-addressed) and overwrites
static/break.pngso EPUB export uses the same image app_settingsextended withbreak_image_sha256andbreak_image_extcolumns (migrationmigrate_app_settings_break_image)- New endpoints:
POST /api/app-settings/break-image;GET /api/app-settingsnow also returnsbreak_image_url - Preview of the current break image visible on the settings page
2026-04-08 (9)
- Grabber: break image fix for DB-stored books
- Break images (
<center><img src="../Images/break.png">) contain a relative EPUB path that does not exist in the DB context - Fix:
storage_modedetermined earlier in_run_scrape; for DB mode/static/break.pngis passed asbreak_img_pathtoelement_to_xhtml, for EPUB mode../Images/break.pngis kept - Additional fix: when a break image is detected in the image loop, the parent (
<center>) is replaced with<hr>instead of decomposing the<img>; this allowselement_to_xhtmlto correctly detect the break after the image loop
- Break images (
2026-04-08 (8)
- New scraper:
IomfatsScraper(scrapers/iomfats.py) foriomfats.org- Matches all
iomfats.orgURLs; no login required - Entry point is a chapter URL (e.g.
.../grasshopper/justhitsend-part1/justhitsend01.html); automatically navigates to the author page to fetch all metadata and chapters - Author page as entry point: raises a user-visible error message
- Detects two structures on the author page:
- Single story: outer
<h3>= book title; chapters directly in<ul> - Multi-part series: outer
<h3>= series name; nested<li><h3>= book title per part; chapters in sub-<ul>
- Single story: outer
- Series index from folder name:
*-part{N}or*-{N}→series_index_hint - Publication status from
<p><small>[...]</small></p>after book title fetch_chapter(): content via<div id="content">; removes<h2>/<h3>headings, chapternav divs, footer elements
- Matches all
2026-04-08 (7)
- New scraper:
CodeysWorldScraper(scrapers/codeysworld.py) forcodeysworld.org- Matches all
codeysworld.orgURLs; no login required - Title from
<h1>; author from<h2>("by …"); fallback to URL slug (/{author}/{category}/filename) - Category from URL path → tag (e.g. "Remembrances")
- Single-file stories (no chapter links): the page itself is the only chapter
- Multi-chapter: links to
.htm/.htmlfiles in the same directory (audio/image links skipped) fetch_chapter(): removes all<h1>/<h2>headings, navigation links ("Back to …"), audio links (.mp3), mailto links
- Matches all
- Nifty scraper: category/subcategory moved from
genres/subgenrestotags
2026-04-08 (6)
- NiftyNewScraper: chapter extraction made more robust
fetch_chapter()now first tries the standard chapter HTML ({url}) and reads<article><p>directly- Fallback added for Next payloads with escaped paragraphs (
\u003cp...\u003c/p) via_extract_escaped_html_paragraphs() - Last fallback remains
?_rsc=1: first_parse_rsc_paragraphs(), then escaped-paragraph fallback again
- Nifty (classic + new): standard boilerplate no longer visible in reader, but preserved in chapter
- Lead/tail detection added for common blocks (e.g.
NOTICE This is a work of fiction…,If you enjoy this story…, donate text) - Detected intro/closing boilerplate removed from visible paragraphs
- Removed text stored as invisible HTML comment:
<!-- NIFTY_HIDDEN_LEAD: ... --><!-- NIFTY_HIDDEN_TAIL: ... -->
- Detection also works when text contains inline HTML (e.g.
<a>or<svg>in donate links)
- Lead/tail detection added for common blocks (e.g.
2026-04-08 (5)
- NiftyNewScraper: chapter content fix
new.nifty.orguses Next.js RSC — chapter content is not present in the static HTML but in the RSC payload (React component tree)fetch_chapter()now fetches{url}?_rsc=1instead of the regular HTML page- RSC parser added:
_parse_rsc_paragraphs(),_rsc_find_paragraphs(),_rsc_text()— parse the RSC stream line by line (format:{hex_id}:{json}), recursively search for["$","p",null,{...}]nodes and extract text fromchildren
2026-04-08 (4)
- NiftyNewScraper added (
scrapers/nifty_new.py) fornew.nifty.org- Matches all
new.nifty.orgURLs; no login required _to_index_url(): strips trailing/N(chapter number) so both index and chapter URLs can be used as entry pointfetch_book_info(): title from<h1>, author from<strong itemprop="name">in author link, dates from<time itemprop="datePublished/dateModified">, tags from all<ul aria-label="Tags">containers (category links and generated tags, deduplicated), description from<meta name="description">, chapter list via<a>links matching/stories/{slug}/N(RSC stream regex as fallback)fetch_chapter(): title from JSON-LD@type: "Chapter", content from<article>; no email header stripping, no line joining, no boilerplate detectionNiftyScraper.matches()updated: excludesnew.nifty.org("nifty.org" in url and "new.nifty.org" not in url)NiftyNewScraperregistered beforeNiftyScraperinscrapers/__init__.py
- Matches all
2026-04-08 (3)
- Settings: develop mode added
- New
app_settingstable (single row,id = 1) withdevelop_modeboolean; created viamigrate_create_app_settings() shared_templates.py: sharedJinja2Templatesinstance for all routers;develop_mode()registered as Jinja2 global so all templates can access it without explicit context injection- All 11 routers now import
templatesfromshared_templatesinstead of each creating their own instance - New endpoints in
routers/settings.py:GET /api/app-settingsandPATCH /api/app-settings - Diagonal DEVELOP banner in the top-left of every page (CSS in
static/sidebar.css, HTML intemplates/_sidebar.html); only visible when develop mode is enabled - All 17 HTML templates:
<title>shows Novela Develop — … when develop mode is active - Settings page: new card with checkbox toggle; page reloads after saving so banner and title apply immediately
- New
2026-04-08 (2)
- Nifty scraper: fix
_strip_email_headersnow tolerates blank lines between header fields- Some Nifty pages place
Subject:after a blank line (Date:\nFrom:\n\nSubject:\n) — the previous implementation stopped at the first blank line causingSubject:to appear as a paragraph in chapter text - Fix: on a blank line, look ahead to check whether a header field still follows; if so, skip the blank line(s) and continue stripping
- Some Nifty pages place
2026-04-08 (1)
- Nifty scraper added (
scrapers/nifty.py)- Matches all
nifty.orgURLs; no login required fetch_book_info: accepts index or chapter URL; normalises to index; title from URL slug, author and publication date from email headers of chapter 1,updated_datefrom email headers of the last chapter; genres/subgenres from URL path (/nifty/{category}/{subcategory}/…)- Boilerplate detection: compares first paragraphs of chapters 1 and 2 after header stripping; matching paragraphs are skipped in all chapters (
preamble_countper chapter dict) fetch_chapter: retrieves<pre>content (fallback: body); subject header stored as invisible HTML comment<!-- Subject: … -->at the top of chapter content; email headers stripped; hard line breaks within paragraphs joined into a single line; scene breaks (***,---, etc.) →<hr/>- Date parsing via
email.utils.parsedate→YYYY-MM-DD
- Matches all
xhtml.py:element_to_xhtmlnow handlesbs4.Commentobjects as XML comments (<!-- … -->);--in the body is sanitised to- -(illegal in XML comments)
2026-04-06 (3)
- Book detail: rating moved from clickable stars to dropdown in the Edit metadata panel
- Stars on the detail page are now purely visual (no longer clickable)
- New
<select id="ed-rating">field in the edit panel, directly below Status openEdit()populates the select with the current rating;saveEdit()callsPOST /library/rating/…if the value has changedrateBook()function removed frombook.js- Overview pages unchanged
2026-04-06 (2)
- Library: cover upload now also supported for DB-stored books
POST /library/cover/{filename}previously returned an error for DB books ("File not found") because no physical file exists- Fix: DB books are now detected via
is_db_filename; cover is stored directly inlibrary_cover_cacheandhas_cover = TRUEset in thelibrarytable —add_cover_to_epubis not called
2026-04-06 (1)
- Search: filter for unread novels / unread shorts
- New
filterparameter onGET /api/search?q=…&filter=all|unread_novels|unread_shorts(default:all) unread_novels: restricts to books with no reading sessions/progress and noShortstagunread_shorts: restricts to books with no reading sessions/progress and with aShortstag- UI: second toggle row (All / Unread novels / Unread shorts) below the Phrase/All words toggle
- Filter persisted in URL (
?filter=…) and restored on page load
- New
2026-04-05 (4)
- Backup: large files (> 148 MB) now uploaded via Dropbox upload session in 100 MB chunks
_dropbox_upload_bytes: files ≤ 148 MB go viafiles_upload(unchanged); larger files viaupload_session_start→upload_session_append_v2→upload_session_finish- Fixes
ApiError: UploadError('payload_too_large')
2026-04-05 (3)
- Filenames: spaces now replaced with underscores when saving new files
clean_segment(common.py) and_clean_segment(reader.py):\s+→_instead of space- Series separator changed from
-to_-_(applies to EPUB, CBR, CBZ and DB filenames) - Existing files are not renamed
2026-04-05 (2)
- Search: removed result limit; added Phrase / All words mode toggle
LIMIT 30removed — all matching chapters are returned- New
modeparameter onGET /api/search?q=…&mode=phrase|words:phrase(default) requires words in order (phraseto_tsquery);wordsrequires all words present in any order (plainto_tsquery) - Toggle in the UI (Phrase / All words) above the results; mode is included in the URL
2026-04-05 (1)
- Export EPUB: double chapter titles fixed — same heading-stripping logic as the reader now applied before passing content to
make_chapter_xhtml - Library: authors and publishers with only archived books now remain visible in the Authors and Publishers list views
renderAuthorsViewandrenderPublishersViewswitched fromactiveBooks()toallBooks— consistent withrenderAuthorDetailandrenderPublisherDetail
2026-04-04 (2)
- Reader: fixed double chapter titles for books where the heading is wrapped in a
<section>element (pandoc-style)- Previous regex only stripped
<h1>–<h4>at the very start of content; pandoc-converted EPUBs wrap headings as<section …><h1>…</h1>— heading was not at position 0 so the regex didn't fire - Added a second
re.subpass that removes the first<h1>–<h4>found directly after an opening<section>or<div>tag, preserving the wrapper element - Same stripping applied in the DB→EPUB export (
export_epub) before passing content tomake_chapter_xhtml
- Previous regex only stripped
- Search: switched from
plainto_tsquerytophraseto_tsqueryin the FTS WHERE clauseplainto_tsqueryANDs all words but treats them as independent terms (any order, any distance) — multi-word queries like "4 years later" matched chapters where the words appeared far apartphraseto_tsqueryrequires all words to appear in sequence;ts_rankandts_headlinestill useplainto_tsqueryfor correct scoring and highlighting
2026-04-04 (1)
- Reader: fixed double chapter titles in DB-stored books
- Chapter endpoint (
GET /library/chapter/{index}/{filename}) now strips all leading<h1>–<h4>tags from stored content before prepending its own<h2 class="chapter-title">— books scraped before front-matter stripping was added to the scraper showed the title (and chapter heading) twice
- Chapter endpoint (
- Library: archived books now visible in author and publisher detail views, with an indicator badge on the cover
renderAuthorDetailandrenderPublisherDetailreverted to useallBooks(including archived) so archived books remain accessible from the author/publisher pages- New
.badge-archivedoverlay (bottom-left of cover): dark circle with archive icon, consistent with existingbadge-statusandread-pilloverlays; added to all three card-rendering code paths
2026-04-03 (3)
- DB chapter editor: Monaco-based editor now supports DB-stored books
GET /library/editor/{filename}handlesdb/…filenames;is_dbflag passed to templateGET /api/edit/chapter/{index}/{filename}andPOST …: DB branches query/updatebook_chaptersdirectly; save callsupsert_chapter(updatescontent_tsvtoo)POST /api/edit/chapter/add/{filename}andDELETE …: DB branches insert/delete withchapter_indexshift viaUPDATE … SET chapter_index = chapter_index ± 1- Title editing: header chapter-name replaced with a text input for DB books;
pendingTitlesmap preserves unsaved titles across chapter switches (parallel topendingContent); title-only dirty chapters correctly saved in Save All insertBreak: scene-break image path is/static/break.pngfor DB books (vs../Images/break.pngfor EPUB)- Fix:
editor.focus()called after content load so Monaco receives keyboard focus immediately - Fix:
header-chapter"Loading…" text suppressed for DB books where that element is hidden book.html: "Edit chapters" button shown forstorage_type = 'db'books
- Search: chapter titles now included in FTS
upsert_chapterprepends title to the plain-text input forto_tsvector:title + " " + stripped_htmlGET /api/search: addedOR LOWER(bc.title) LIKE LOWER('%…%')fallback for chapters whose title matches but content doesn't- Startup migration
migrate_rebuild_chapter_tsv_with_title()rebuilds existingcontent_tsvvalues to include titles
- Grabber: added DB/EPUB storage toggle on the Convert page
- UI toggle above Convert button ("Save as: DB | EPUB file");
storageModeJS variable sent in POST body POST /convert: readsstorage_modefrom body; stored in job as'db'or'epub'_run_scrape: EPUB path builds chapters viamake_chapter_xhtml, callsmake_epub, writes file, callsupsert_book(storage_type='file'); DB path unchangeddoneSSE event includesstorage_type;conversion.jsupdates the download button label/action accordingly
- UI toggle above Convert button ("Save as: DB | EPUB file");
- EPUB → DB conversion: fixed double chapter title
_epub_body_innerstrips the first<h1>/<h2>/<h3>heading from each chapter body before storing; the editor prepends its own heading, so storing the EPUB heading too caused it to appear twice- Fix for
NavigableStringcrash:getattr(child, "name", None) is Noneused instead ofhasattr(child, "name")—NavigableStringhasname = Nonebut nodecompose()method
- Sidebar: Search link styling fixed
- Stray
<li>Search</li>moved inside the Library<ul class="sidebar-nav">(was outside, causing incorrect HTML structure) sidebar.css: addeda:visited { color: var(--text-dim) }anda.active:visited { color: var(--accent) }to prevent the browser's default purple visited color
- Stray
2026-04-03 (2)
- DB-stored books (Fase 4–6): EPUB→DB conversion, DB→EPUB export, full-text search
- Fase 4 — EPUB-to-DB conversion:
POST /api/library/convert-to-db/{filename}converts an existing on-disk EPUB to a DB-stored book; extracts chapters via_epub_body_inner(rewrites img src to imagestore URLs), migrates all child rows (book_tags, reading_progress, reading_sessions, bookmarks, library_cover_cache) to the newdb/…filename using INSERT→UPDATE→DELETE to respect FK constraints, then deletes the EPUB file - Fase 5 — DB→EPUB export:
GET /api/library/export-epub/{filename}builds and streams an EPUB from DB content;_rewrite_db_images_for_epubrewrites/library/db-images/…URLs back toOEBPS/Images/…paths, deduplicating by sha256;Content-Disposition: attachmentresponse - Fase 6 — Full-text search: new
routers/search.pywithGET /search(page) andGET /api/search?q=…(FTS overbook_chapters.content_tsvviaplainto_tsquery('simple', q),ts_headlinefor snippets,ts_rankfor ordering, LIMIT 30, excludes archived); newtemplates/search.htmlwith highlight (<mark>), "Read here" link (?bm_ch=N&bm_scroll=0), and "Book detail" link; Search entry added to sidebar book.html: DB books show "Export EPUB" instead of "Download"; "Edit EPUB" and "Convert to DB" buttons only shown for.epubfiles; delete modal text differs for DB vs file booksPATCH /library/book/{filename}: DB book branch added — skips file move, recomputes syntheticdb/…filename viamake_rel_path, applies same FK-safe rename pattern, updatesbook_chaptersandbookmarksin addition to standard child tables
- Fase 4 — EPUB-to-DB conversion:
2026-04-03 (1)
- DB-stored books (Fase 1–3): grabber now stores scraped books in PostgreSQL instead of EPUB files on disk
- New
book_chapterstable:filename FK, chapter_index, title, content TEXT, content_tsv TSVECTOR; GIN index oncontent_tsvfor future FTS - New
book_imagestable:sha256 PK, ext, media_type, size_bytes; content-addressed imagestore atlibrary/images/{sha2}/{sha256}{ext} - New
storage_type VARCHAR(10) DEFAULT 'file'column onlibrary; DB-stored books use'db' - New utilities in
common.py:is_db_filename,write_image_file,store_db_image,html_to_plain,upsert_chapter,ensure_unique_db_filename;make_rel_pathnow handlesmedia_type="db"→ syntheticdb/{pub}/{auth}/...filename upsert_bookandlist_library_jsonupdated to includestorage_type- Grabber:
_run_scrapestores chapters inbook_chapters, chapter images in imagestore (absolute/library/db-images/URLs embedded in HTML), cover inlibrary_cover_cache; no EPUB file written - New
GET /library/db-images/{path:path}endpoint serves imagestore files - Reader:
GET /library/chapters/andGET /library/chapter/have DB branches forstorage_type='db'books (querybook_chaptersdirectly) - Reader page (
/library/read/), book detail page, mark-read, and rating endpoints all handle DB filenames (no file existence required) - Cover endpoints (
/library/cover/,/library/cover-cached/) serve DB books fromlibrary_cover_cache
- New
2026-04-02 (1)
- Added Restore functionality to the Backup page
- New
GET /api/backup/snapshotsendpoint: lists available Dropbox snapshots (name + date parsed from filename, no downloads needed) - New
GET /api/backup/snapshots/{snapshot_name}/filesendpoint: loads a snapshot from Dropbox and returns all files with path, size, sha256, and whether the file currently exists locally - New
POST /api/backup/restoreendpoint: downloads file objects from Dropbox, writes to disk, and re-indexes viascan_media+upsert_book; returns per-file result with errors - New "Restore" card on the backup page: snapshot dropdown (auto-loaded on page open), file list with filter/search, per-file "Restore" button, multi-select + "Restore selected", on-disk indicator, inline status feedback
- After restore, the file list refreshes to reflect updated on-disk state
- New
2026-03-29 (10)
- Duplicates: fixed
updateCountscrashing with a TypeError (g.books.length→g.length); the crash preventedrenderGridfrom running, so the duplicates view never rendered and the counter was stale
2026-03-29 (9)
- Duplicates: volume-aware duplicate detection — a book is only a duplicate when title + author + volume all match
_duplicateGroupsinlibrary.js(Duplicates view): key now includesseries_indexwhen > 0, so different volumes of the same series are no longer grouped as duplicatespreloadingrabber.py(Grabber): when the scraper returns aseries_index_hint, the DB lookup only flags a match when title + author + volume all match; falls back to title + author when no volume is known — same logic as the bulk importer
2026-03-29 (8)
- Shared code: eliminated all remaining duplication across templates and JS files
- CSS custom properties: extracted single
:root { }block intostatic/theme.css; removed duplicate inline:rootfrom all 15 templates and fromlibrary.css,book.css—editor.csskeeps editor-specific vars (--danger,--header-h,--panel-w),reader.htmlkeeps page-specific vars (--header-h,--footer-h,--content-w),backup.htmlkeeps (--ok,--warn,--err) - Cover helpers: moved
strHash,COVER_PALETTES,makePlaceholderCover,wrapText,truncatefromlibrary.jsandbook.jsintobooks.js; removed fromhome.htmlandfollowing.html - HTML escape:
esc()added tobooks.js; removed fromlibrary.js,editor.js, and all 8 templates that defined it inline - SSE/EventSource: extracted shared
connectConversionStream(job_id)andaddLog()into newstatic/conversion.js; bothindex.htmlandgrabber.htmlnow call the shared function (removed ~70 duplicate lines)
- CSS custom properties: extracted single
2026-03-29 (7)
- Search: extracted shared book helpers and search logic into
static/books.js_filenameBase,bookTitle,bookAuthor,tagValuesByType,bookGenres,bookSubgenres,bookPlainTags,filterBooks,setupSearchInputmoved fromlibrary.jsandhome.htmlto a single shared file- Both pages now use identical search behaviour: Enter to search, × to clear
- Both pages now search across title, author, publisher, genre, sub-genre, and tag
2026-03-29 (6)
- Search: changed from search-as-you-type (250 ms debounce) to Enter-to-search on both Library and Home — prevents iPad keyboard from locking up on large collections
2026-03-29 (5)
- Accent colour updated to match logo orange (
#ffa20e); secondary accent updated to#ffb840- Applied across all CSS files, templates, and inline styles
2026-03-29 (4)
- Branding: added logo, favicon, and Apple touch icon to all pages
- Static assets:
logo.png(sidebar),favicon.ico(16×16),favicon-32.png(32×32),favicon-256.png(256×256),apple-touch-icon.png(180×180 with#0f0e0cbackground) - Favicon
<link>tags added to all 15 templates - Sidebar: image logo (
logo.png) placed next to the existing "Novela" wordmark using flexbox apple-touch-icon.pnguses dark#0f0e0cbackground — renders as a native-looking iOS home screen icon
- Static assets:
2026-03-29 (3)
- Dockerfile: replaced
unrar-freewith proprietaryunrar(RARLAB v6.2.6) from Debian non-free — fixes "Failed to read enough data" errors on RAR archives using newer compression methods
2026-03-29 (2)
- CBR reader: detect archive format via magic bytes instead of file extension —
.cbrfiles that are actually ZIP or 7-zip archives now open correctly; addedpy7zrdependency for 7-zip support
2026-03-29 (1)
- Bulk Import: duplicate check now volume-aware — books with the same title+author but a different volume number (e.g. recoloured reprints) are no longer flagged as duplicates;
volumeis included in the API call and matched againstseries_indexin the DB
2026-03-28 (11)
- Bulk Import: duplicate detection against existing library
- New
POST /api/bulk-check-duplicatesendpoint: case-insensitive title+author+volume match, single SQL query with OR conditions for all pairs - Duplicate rows highlighted in red in the preview table; a skip checkbox appears per duplicate row
- Stats bar shows "X duplicates · Skip all · Import all" action buttons
- Duplicate rows are skipped by default; user can toggle individual rows or use bulk actions
startImport()filters out skipped rows before batching; skipped files appear in the result summary as "Duplicate – skipped"- If all rows are skipped (nothing to import), result shown immediately without sending any request
- New
2026-03-28 (10)
- After "Remove from New" succeeds, the library is now reloaded from the server (
loadLibrary()) so the New list updates immediately without a manual refresh
2026-03-28 (9)
- Bulk delete is now batched: new
POST /library/bulk-deleteendpoint accepts a JSON list of filenames, deletes files and removes DB rows in one query per batch; JS sends 20 files per batch with a progress bar in the confirmation dialog
2026-03-28 (8)
- Disk usage warning in sidebar:
GET /api/diskreturns partition usage for the library directory; sidebar polls every 60 s and shows a warning bar (amber ≥ 85% or < 2 GB free, red ≥ 95% or < 500 MB free) above the backup status bar
2026-03-28 (7)
- Fixed
mark-reviewedJS: UI (allBooks update + renderGrid) now only runs after confirmed server success;catchno longer fires a false error after a successful response where renderGrid throws
2026-03-28 (6)
- Bulk Import: rewrote pattern editor from token-pills to free-text
%placeholder%syntax- Pattern input: free-text field; placeholders:
%series%%volume%%title%%year%%month%%day%%author%%publisher%%ignore% - Colored chip row: click to insert at cursor position; drag onto input also supported
- Colored live preview below the pattern input; regex-based parser replaces delimiter+token logic
- Shared metadata now wins over filename-parsed values (previously filename won)
- All UI text translated from Dutch to English
- Pattern input: free-text field; placeholders:
2026-03-28 (5)
- Status badge (top-right cover) and want-to-read star (top-left cover) now use dark fill
rgba(15,14,12,0.82)+box-shadow: 0 0 0 2px #0f0e0cring — always readable regardless of cover colour
2026-03-28 (4)
- Added
Temporary Holdstatus; renamedHiatus→Long-Term Hold- Startup migration
migrate_rename_hiatus()converts existing DB records automatically - Status colours: Complete=green, Ongoing=blue, Temporary Hold=amber, Long-Term Hold=orange
statusBadgeHtml()helper inlibrary.jsreplaces three identical inline badge blocks- Grabber:
Temporary-Hold(gayauthors.org) now maps toTemporary Hold;Long-Term Holdpasses through unchanged - Status dropdowns updated in Book Detail and Bulk Import
- Startup migration
2026-03-28 (3)
- Added Bulk Import page (
/bulk-import) for batch importing CBR/CBZ/EPUB/PDF files with filename-based metadata parsing- Configurable token pattern: Serie, Volume, Titel, Jaar, Auteur, Uitgever, Negeer (in any order)
- Configurable delimiter (default
-); year token right-anchored when last so title absorbs overflow segments - Live test-parse box: type any filename and see how it parses before selecting files
- Shared metadata card: author, publisher, status, genres, tags applied to all files (overridden by pattern tokens or manual edits)
- Preview table: all parsed fields editable via contenteditable cells; warning indicator for rows with fewer segments than tokens; sorted by filename
- Batch upload in groups of 5 with progress bar (N / total files processed)
- Result summary with list of skipped files and reasons
- New
routers/bulk_import.py:GET /bulk-import,POST /library/bulk-import - Sidebar link under Tools between Book Builder and Credentials
This file tracks changes on the develop line.
changelog.md can later be used for release summaries.
2026-03-28 (2)
- Performance: library page now loads instantly for large collections (1000+ books)
IntersectionObserverdefers both cover image loading and placeholder canvas drawing until cards enter the viewport — eliminates hundreds of upfront canvas ops that blocked the initial renderETagcaching on/library/list: server returns304 Not Modifiedwhen nothing changed, client skips JSON parse and re-download- Single DOM pass in
renderBooksGrid,renderDuplicatesView,renderSeriesDetail: canvas and img set up viacard.querySelectorimmediately afterinnerHTML, removing a second iteration withdocument.getElementByIdper card book_tagsjoined viajson_aggin the mainlist_library_json()query, eliminating a separateSELECT * FROM book_tagsquery and Python merge looploadLibrarynow shows an error message instead of staying stuck on "Loading…" when the fetch or render fails
2026-03-28 (1)
- Added Following page (
/following): track external author URLs outside Library and Tools- New
authorstable:name(unique),url,created_at,updated_at - New
routers/following.py:GET /followingpage,GET /api/following(all authors + URL + book count + last added),POST /api/following/{name}(set/clear URL) - Sidebar: new Following section between Library and Tools; counter shows number of followed authors
- Following page: two tabs — Following (authors with URL) and All Authors; inline URL editing with Enter/Escape keyboard support; Visit button opens external URL in a new tab; author name links to library author view
- New
- Added Incomplete view to Library (
#incomplete): shows all non-archived books wherepublication_status ≠ Complete; sidebar counter included; entry placed after New in the Library section
2026-03-27 (1)
- Convert page: duplicate warning shown after loading metadata when a book with the same title+author already exists in the library; warning includes a link to the existing book; user can still proceed with conversion
- Library: added Duplicates section to sidebar (between Rated and Statistics); counter shows total number of books that are part of a duplicate group (same title+author, case-insensitive); Duplicates view groups books by title+author with a subheading per group
- Fixed Duplicates view not loading covers: card renderer now uses the same canvas + two-pass img/
makePlaceholderCoverpattern asrenderBooksGrid
2026-03-26 (2)
- Fixed Book Builder page showing white background:
library.cssadded tobuilder.htmlto load:rootCSS variables and darkbodybackground; all CSS variable references inbuilder.cssaligned with library theme names (--text,--surface,--surface2,--text-dim,--border,--accent,--sidebar)
2026-03-26 (1)
- Added Book Builder: create EPUB books manually from scratch via a WYSIWYG editor
- New
builder_draftstable (id UUID,title,author,publisher,source_url,chapters JSONB) build_epub()inepub.py: builds a standards-compliant EPUB 2.0 ZIP from title/author/publisher/chapters; embeds inline CSS andbreak.pngif presentnormalize_wysiwyg_html()inxhtml.py: converts contenteditable HTML to EPUB-safe XHTML; handles scene-breaks,<blockquote class="author-note">, inline formatting, and<hr>→ break imagerouters/builder.py: draft CRUD, chapter CRUD (GET/POST/PUT/DELETE), normalize preview endpoint, publish endpoint (normalizes all chapters → builds EPUB → writes tolibrary/epub/→ upserts to DB → deletes draft → redirects to book detail)templates/builder.html+static/builder.js+static/builder.css: index page (new draft form + draft list) and editor (chapter panel, contenteditable pane, toolbar with bold/italic/underline/blockquote/author-note/scene-break/normalize, autosave every 30 s, Ctrl+S, publish button)- Book Builder link added to sidebar Tools section (between Convert and Credentials)
.author-noteblockquote style added tostatic/epub-style.css
- New
2026-03-25 (20)
- Added Rated section to library sidebar: shows all non-archived books with
rating > 0, sorted by rating descending then title alphabetically; badge displays total count; navigable via#ratedURL hash
2026-03-25 (19)
- Fixed series index 0 not displaying in series slot view, grid cards, list volume column, and book detail:
- Series slot labels now show
#0for all slots when the series has at least one positively-indexed sibling (consistent label height across all cards) - Grid card and list volume column use the same cross-book heuristic (
indexedSeriesSet) to show[0]where appropriate - Book detail page queries whether any sibling in the same series has
series_index > 0(series_is_indexed) and shows[0]accordingly
- Series slot labels now show
2026-03-25 (18)
- Added autocomplete for Author, Publisher, and Series in the book edit panel: typing shows a filtered dropdown of existing values from the library; selecting a value fills the field (same dropdown styling as genres/tags)
- Status field now defaults to "Complete" when opening the edit panel for a book that has no status set yet
2026-03-25 (17)
- Fixed CBR reader showing only first page:
cbr_page_countwas missing from thecbrimport inreader.py;/api/cbr/info/was returning an error, causingpage_countto fall back to 1 and the Next button to remain disabled
2026-03-25 (16)
- Fixed CBR/CBZ reader stuck on loading: added
/api/cbr/info/{filename}endpoint returning{page_count}, and added a CBR/CBZ branch inreader.htmlthat mirrors the PDF paged reader (page images served via/library/cbr/{filename}/{page})
2026-03-25 (15)
- Added series volume 0 and letter suffix support (e.g. "21a", "21b"):
- New
series_suffix VARCHAR(10)column added tolibrarytable viamigrate_series_suffix series_indexlower bound changed from 1 to 0 throughout (index 0 = special/prequel edition)- Volume field in the book editor changed from
type="number"totype="text"— accepts "0", "1", "21a", etc. - Server parses the combined volume string into
series_index(INTEGER) +series_suffix(VARCHAR) viaparse_volume_str - File naming includes suffix:
021a - Title.epub novela:series_suffixmeta tag written to/read from EPUB OPF for persistence across rescans- Series detail view: books sorted by
(series_index, series_suffix); slot labels show "21a"; index 0 shown as slot when any sibling has index > 0 - Grid cards, list view Volume column, author/publisher sort all include suffix
- New
2026-03-25 (14)
- Added multi-select and bulk delete to
All booksList view:- Checkbox column added to the list table (List mode only; Grid mode unchanged)
- Select all / Clear all / Clear selection controls in the controls bar
Shift+clickrange selection on checkboxesDelete selectedbutton (red, disabled when nothing is selected) triggers a confirmation dialog; confirmed deletions remove files from disk and database, then reload the library
2026-03-25 (13)
- Fixed EPUB cover replacement reverting to original after upload:
GET /library/cover/{filename}for EPUBs now checks the DB cover cache first (populated on upload) before falling back to extracting from the EPUB file; the cache is also warmed on cache-miss so subsequent requests are fast.add_cover_to_epubfully rewritten: locates the OPF viaMETA-INF/container.xml, finds the existing cover image in the OPF manifest, removes it from the ZIP, and writes the new cover in the same directory — works for any EPUB directory structure (OEBPS/,EPUB/, etc.).
2026-03-25 (12)
- Fixed chapter images not loading in EPUBs that use a non-standard root directory (e.g.
EPUB/instead ofOEBPS/): image paths are now passed as full ZIP paths instead of stripping the root segment, and the image endpoint no longer hardcodes anOEBPS/prefix. Case-insensitive fallback retained for EPUBs with mismatched image folder casing.
2026-03-25 (11)
- Fixed reader progress bar jumping to 100% immediately when navigating to the second chapter in short EPUBs (2 chapters): changed progress formula denominator from
total - 1tototalso 100% is only reached after fully scrolling through the last chapter. Same fix applied to PDF page progress.
2026-03-25 (10)
- Added bookmark feature:
bookmarkstable in DB (migrate_create_bookmarks):filename,chapter_index,scroll_frac,chapter_title,note,created_at- New API endpoints:
GET/POST /library/bookmarks/{filename},PATCH/DELETE /library/bookmarks/{id},GET /api/bookmarks - Bookmark button in reader header (orange, bookmark icon): opens modal with optional note field
- Bookmark modal closes on Escape, backdrop click, or Cancel
- Reader supports
?bm_ch=N&bm_scroll=FURL params to jump directly to bookmarked position (overrides saved progress) - Library sidebar: Bookmarks section in Library nav with live count badge
- Library
#bookmarksview: card list showing cover, book title, author, chapter, note, date, Go-to and Delete buttons
2026-03-22
- Added blueprint/technical documentation structure in
docs/. - Completed router split and bootstrap structure (
main.py, routers, migrations, DB pool). - Expanded media support to EPUB/PDF/CBR/CBZ in import and scan flows.
- Expanded Home UI with:
- import dropzone for EPUB/PDF/CBR/CBZ
- search functionality
- alignment matching Library (search top-right, dropzone below)
- Updated Library import texts and drag/drop filtering for multi-format support.
- Expanded Library
Newview with:Grid/Listtoggle- column visibility filter in
List - multi-select + bulk
Remove from New - selection only in
Listmode Shift+clickrange selection on checkboxes
- Added route:
POST /library/new/mark-reviewed(bulk setneeds_review=false). - Improved library performance:
/api/libraryfast-path (no full rescan on every page load)- optional
rescan=true/include_file_info=true - SQL optimizations in
list_library_json() - additional DB indexes for scale
- Restored
/api/homefull dataset output:continue_readingshorts_unreadnovels_unreadshorts_readnovels_read
- Explicitly filtered series books out of Home sections.
- Corrected Home read ordering:
shorts_readandnovels_readnow show oldest first (ORDER BY MAX(read_at) ASC). - Restored Statistics page by returning the full
/api/statspayload required for charts, favorites, top books, and reading history. - Improved backup implementation:
- Dropbox token stored encrypted in DB
- Dropbox backup root configurable via web UI and stored encrypted in DB
- versioned snapshots + object-store deduplication in Dropbox (
library_snapshots/library_objects) - configurable snapshot retention (
snapshots to keep) via backup settings - object pruning based on retained snapshots
- scheduled backup (enable + interval in hours)
- backup runs as a background process, so page navigation does not block execution
- stale running state recovery after restart/crash (old running logs marked interrupted/error)
- dry-run support in the new flow
- Updated Docker image with
postgresql-clientforpg_dump. - Multiple test builds pushed to
gitea.oskamp.info/ivooskamp/novela:dev.
2026-03-25 (9)
- Fixed backup progress not visible immediately after clicking Run Live Backup: sidebar
loadBackupProgress()is now called directly from the backup page after a successful start - Fixed backup status timestamp showing wrong relative time (e.g. "1h ago" for a recent backup): server UTC timestamps without timezone suffix are now correctly interpreted as UTC in the browser
- Fixed backup status bar using browser-default purple link color: now uses
var(--accent)orange consistent with the rest of the UI - Added password manager ignore attributes to App Key and App Secret fields:
data-1p-ignore(1Password),data-lpignore="true"(LastPass),data-form-type="other"(1Password v8)
2026-03-25 (8)
- Added Dropbox OAuth2 refresh token support (no redirect URI needed):
- New endpoints:
POST /api/backup/oauth/prepare(generates auth URL) andPOST /api/backup/oauth/exchange(exchanges code for refresh token) _dbx()now prefers refresh token mode (app key + secret + refresh token) with automatic renewal; falls back to legacy access token for backwards compatibility- App key and secret stored encrypted in
credentialstable (dropbox_app_key,dropbox_app_secret) DELETE /api/backup/credentialsnow also cleans updropbox_app_keyanddropbox_app_secret- Backup page updated with two-step OAuth flow UI (app key/secret → auth URL → paste code)
- Settings status shows
• refresh tokenor• legacy tokenindicator
- New endpoints:
2026-03-25 (7)
- Added backup progress tracking:
GET /api/backup/progressreturns{running, done, total, phase}; sidebar shows live file count (e.g.312 / 652 · uploading) polling every 3 s while running
2026-03-25 (6)
- Fixed
GET /api/backup/healthmissingschedule_enabledandschedule_interval_hoursin response (values were read but not returned; Health card showed "disabled" even when schedule was on) - Added backup status indicator in sidebar above Rescan library button: coloured dot + status text (OK/running/failed/never), links to
/backup, auto-refreshes every 8 s while running, shows time-ago for last successful backup
2026-03-25 (5)
- One-time path migration: all 571 existing library files moved to correct format-prefixed structure (
epub/,pdf/,comics/) and all DB references updated (migrate_paths.py --execute) - Empty old publisher root directories pruned after migration
- Recovery script (
recover_decock049.py) written; confirmed file was added after last Dropbox backup so not recoverable — to be re-imported manually
2026-03-25 (4)
- Added {publisher} directory to PDF and CBR/CBZ paths:
pdf/{publisher}/{author}/{title}.pdf,comics/{publisher}/{author}/{title}.cbr/cbz— consistent with EPUB structure
2026-03-25 (3)
- Fixed CBZ extension in import:
common.make_rel_pathalways generated.cbrfor CBZ files; now acceptsextparameter;library.pypasses actual suffix so CBZ files land atcomics/{author}/{title}.cbz - Added missing
GET /download/{filename}endpoint (referenced in book.html but was 404) - TECHNICAL.md fully rewritten: added File Storage Paths section, complete endpoint lists for all routers including settings.py, corrected path documentation
2026-03-25 (2)
- Fixed PDF metadata editing (PATCH /library/book):
_sync_epub_metadatais now only called for.epubfiles; PDFs update DB only_make_rel_pathnow includes the format prefix matching import: EPUB →epub/{publisher}/{author}/…, PDF →pdf/{author}/{title}.pdf, CBR/CBZ →comics/{author}/{title}{ext}; previously files were moved outside their format directory on metadata save
- Fixed PDF reader (infinite loading screen):
reader_pagenow passesformat(epub/pdf/cbr/cbz) toreader.html- Added
GET /api/pdf/info/{filename}endpoint returning{"page_count": N} reader.htmlbranches onFORMAT: PDFs render page images from/library/pdf/{filename}?page=N, EPUB flow unchanged- PDF progress tracked per page; keyboard and button navigation work identically to EPUB
Edit EPUBbutton in Book Detail hidden for non-EPUB files
2026-03-23
- Added
All booksGrid/List toggle in Library:- same columns as
Newview (Publisher, Author, Series, Volume, Title, Has cover, Updated, Genres, Sub-genres, Tags, Status) - column visibility filter in
Listmode - no selection checkboxes or bulk actions
- view mode and column visibility persisted separately in
localStorage(novela.all.viewMode,novela.all.visibleColumns)
- same columns as
- Added 1–5 star rating for books:
- stored in the database (
rating SMALLINT DEFAULT 0) - written to EPUB OPF as
<meta name="novela:rating" content="N"/>on rating change - written to CBZ
ComicInfo.xmlas<NovelaRating>N</NovelaRating>on rating change - CBR and PDF are DB-only (file format constraints)
- rating is recovered from file metadata during rescan/DB rebuild (
upsert_bookpreserves file rating over default 0) - stars displayed under the cover (outside
.book-info) in all grid views (Library, Home) - stars in grid cards are display-only (no click) to prevent accidental taps while scrolling
- stars in Book Detail are interactive (larger, 1.1rem), clicking same star removes rating
- star color: amber (
#c8a03a) for filled,rgba(200, 160, 58, 0.25)for unfilled — consistent across Library, Home, and Book Detail
- stored in the database (
- Added text colour setting to reader hamburger menu:
- 5 warm-tone presets from bright (
#e8e2d9) to dim (#938d86) - active preset shown with accent-coloured ring
- choice persisted in
localStorage(reader-text-colour) and restored on next open
- 5 warm-tone presets from bright (
- Increased spacing between hamburger button and back link in reader header (
margin-left: 1rem) to prevent accidental taps - Removed
Cover Missingauto-tag:- tag is no longer added on import, rescan, or grabber download
ensure_cover_missing_tag()removed fromcommon.py,library.py, andgrabber.py- startup migration removes all existing
Cover Missingtags from the database
- Fixed Tags/Genres/Sub-Genres not saving in book edit panel on desktop:
,(comma) now acts as a confirmation key alongside Enter inPillInputflush()added toPillInput: any text still in the input field is auto-confirmed when Save is clicked
- Fixed tag/genre search and tag-pill navigation being broken:
renderGenreViewwas filtering onb.genres(non-existent field); now usesbookGenres(),bookSubgenres(),bookPlainTags()renderSearchResultshad the same bug; search now covers title, author, genres, sub-genres, and tags