37 Commits

Author SHA1 Message Date
Andrew Doering
816a735cde Add a {Language} naming template variable, and consolidate language resolution (#1142)
Fixes #1138
Fixes #1141

## Problem

Two language editions of one book resolve to the same canonical title,
so they render to the same path and the second gets a `_1` collision
suffix. Audiobookshelf treats a folder as exactly one library item, so
the pair becomes a single book with both files as tracks and a summed
runtime.

Shelfmark already parses and displays the language. It just never
reached the template engine.

## `{Language}` template variable

A template like `{Author}/{Title}{ (Language)}/{Author} - {Title}` now
yields:

```
/library/J K Rowling/Harry Potter (sv)/J K Rowling - Harry Potter.m4b
/library/J K Rowling/Harry Potter/J K Rowling - Harry Potter.m4b
```

The untagged edition's path is byte-identical to today, so no existing
layout shifts.

Three details worth flagging:

**The value is casefolded.** On a case-insensitive filesystem `(SV)` and
`(sv)` would collapse back into one folder, reintroducing the exact
collision being fixed.

**Values meaning "we don't know" render nothing** rather than producing
`Project Hail Mary (unknown)` folders. Anna's Archive reports that
string literally (`direct_download.py`, `language = detected or
"unknown"`).

**The frontend wasn't sending the release language at all**, so the
token would have stayed empty for exactly the audiobook sources in the
report. Prowlarr and AudiobookBay do not put language in `extra` the way
`direct_download` does, hence the payload plumbing. It reads
`release.language`, never `book.language` — the latter is the provider's
canonical edition and would mislabel a translation, with a regression
test for that specifically.

Not gated to audiobooks: Calibre-Web-Automated stages ingested files by
basename and discards folder structure, so the rename (filename)
template is the only lever those users have. Verified that form works:
`J K Rowling - Harry Potter (sv).epub`.

## Language consolidation (#1141)

Three release sources each carried their own alias map, all resolving to
the same ISO 639-1 codes, alongside a bundled database that only one of
them used. Adding a language meant editing three places.

Aliases now live in `data/book-languages.json` beside the code and name
they belong to, and `shelfmark/core/languages.py` resolves any of them —
two-letter code, ISO 639-2 three-letter in either the bibliographic or
terminological form, or English name. Prowlarr and AudiobookBay drop
their tables. Direct Download keeps its own path-parsing heuristics,
including the ambiguous short codes that collide with English words
(`de`, `en`, `no`, `in`), and takes only the alias data.

This also closes a coverage gap. MyAnonamouse offers 62 languages;
Prowlarr mapped 37, and an unmapped code is *dropped* rather than passed
through, so the other 25 carried no language at all — leaving
`{Language}` empty and the collision unfixed for Latin, Farsi, Tamil,
Urdu and the rest. Seven languages MAM offers had no database entry at
all: Bosnian, Burmese, Estonian, Icelandic, Manx, Scottish Gaelic,
Sanskrit.

Also fixes the Traditional Chinese code, which used a U+2011
non-breaking hyphen. Nothing compares against the ASCII spelling today
so it was latent, but it would silently defeat the first thing that did.

## Validation

Verified end to end against a live Prowlarr and MyAnonamouse, not just
unit tests. A real search returning both an English and a Swedish
edition, through the actual `queue_release` → `DownloadTask` → naming
path:

```
STEP 1  real MAM search        -> 37 releases, languages: ['en', 'sv']
STEP 3  queue_release          -> task.language='sv'
STEP 4  build_metadata_dict    -> metadata['Language']='sv'
STEP 5  build_library_path     -> /library/J K Rowling/Harry Potter (sv)/...
two language editions resolve to DIFFERENT folders: True
```

The refactor is pinned by a snapshot of both per-source maps taken
*before* they were deleted. All 131 aliases are asserted to still
resolve to the same code, one parametrised test each, so a regression
names the specific alias.

Also verified: the filename-only template, the retry round-trip
(`serialize_task_for_retry` → `_restore_task_from_retry_payload`, plus a
legacy payload with no `language` key), and placeholder handling.

Added a `KNOWN_TOKENS` ordering invariant test — `find_placeholder()`
does a substring `.find()` in list order and nothing protected that
contract, so a future token in the wrong position could silently shadow
an existing one. And a lockstep guard on the frontend, since
`KNOWN_TOKENS` is hand-duplicated in TypeScript.

**One caveat worth stating.** Three MAM codes are confirmed by
observation (`ENG`→`en`, `SWE`→`sv`, `MAL`→`ml`, the last from a real
`[MAL / EPUB]` Tagore release). The remaining ~59 are derived from ISO
639-2 rather than observed, because MAM's catalogue is overwhelmingly
English — enabling 27 extra languages still yielded only one non-English
hit across 258 results. Mitigated rather than closed: both 639-2
variants are present for every language where they differ, and a wrong
alias is an unused entry while a missing one loses the language. Happy
to correct any code a maintainer knows differs.

## Test results

2056 Python tests pass (up from 1906). Frontend typecheck, lint, format
and 126 unit tests pass.

Pre-existing failures on my machine, unchanged by this branch and
unrelated: `tests/bypass/` needs `seleniumbase`, and
`tests/config/test_entrypoint_permissions.py` uses bash-4 syntax that
macOS bash 3.2 rejects.

---------

Co-authored-by: delize <4028612+delize@users.noreply.github.com>
Co-authored-by: CaliBrain <calibrain@l4n.xyz>
2026-07-28 15:19:59 -04:00
CaliBrain
a1367f431d Ship Prowlarr per-entry rows as opt-in, not the default (#1145)
#1140 fixed the guid-only dedup that hid results from filter-specific
indexer entries, but shipped the new behaviour on by default:
PROWLARR_COLLAPSE_DUPLICATES defaulted off, so every existing Prowlarr
user got extra rows for any release that two indexer entries both
returned, and the setting only let them opt back into what they already
had.

Default it on. The dedup key stays indexer-qualified, so the entries are
still distinct internally; collapse then merges them back to one row,
resolved by the Prowlarr priority rather than by query order as before.
The visible result set matches what users had prior to #1140, and anyone
who wants the per-entry rows (freeleech and the like) turns the setting
off.

Beyond the noisier list, the default mattered because split rows differ
only by indexer name while sharing a title, size and peer count. Two of
them have distinct source_ids, so the queue's duplicate guard does not
fire, and the second grab's find_existing() matches the first by
infohash and runs post-processing over the same download again,
delivering the book twice.

The search-side fallback in config.get(..., True) is flipped to agree
with the field default. In production the field default governs, since
the config cache is seeded from the registry and the fallback only
applies to an unregistered key. The source tests monkeypatch config.get
with a plain dict lookup, though, so the fallback is what they exercise:
leaving it False would have kept every default-behaviour test asserting
the opposite of what ships. A new test pins the two together.

The deduplication tests now opt out explicitly, since they assert the
split itself. test_collapse_off_by_default_keeps_both_rows becomes a
pair, one for the untouched setting collapsing to a single row and one
for opting out.

docs/environment-variables.md is regenerated rather than hand-edited. It
was already stale on main, so it also picks up RTORRENT_AUDIOBOOK_LABEL,
DIRECT_DOWNLOAD_LANGUAGE_FROM_PATH, and reworded IRC_SEARCH_BOT and
RTORRENT_LABEL text from earlier merges.

The rest is fallout from the ruff 0.16.0 bump in #1139, which enabled a
much larger default rule set and started formatting Python code blocks
in Markdown: _find_existing_alias_user() uses min() instead of
sorted()[0] (FURB192, currently failing Python Quality on main), and the
two READMEs get their code blocks reformatted.
2026-07-28 01:23:31 -04:00
Andrew Doering
a4086f5e06 Keep Prowlarr results from distinct indexer entries separate (#1140)
Results were deduplicated on `guid` alone. When one tracker is
configured in Prowlarr as several indexer entries differing only by a
server-side search filter, all of them return the same guid for the same
torrent, so every entry but the first was silently discarded. Freeleech
and other filter-specific releases became invisible, replaced by the
unfiltered entry's copy, and which copy survived depended on query
ordering rather than user intent.

Include the indexer id in the dedup key so the entries stay distinct.

Release.source_id is qualified the same way. It keys the release cache
and becomes the download task id, so rows sharing a guid would otherwise
collide and a grab would route through whichever entry cached last,
defeating the point of showing them separately. The handler's task
matcher still accepts a bare guid or infoUrl so tasks queued before this
change still resolve.

Ordering now follows the priority already configured in Prowlarr (1-50,
lower preferred) rather than a new setting, since users curate that
ranking there and filtered entries are typically ranked ahead of their
unfiltered counterparts. The enabled-indexer list is fetched once and
reused for the enrichment check, so this costs no extra round trip.
Releases carry extra.indexer_priority, and the sort dropdown gains an
"Indexer priority" entry, ascending, alongside the existing alphabetical
"Indexer" sort; SortOption grew a default_direction for that, defaulting
to desc so "Peers" is unchanged.

PROWLARR_COLLAPSE_DUPLICATES (default off) optionally collapses a
release back to one row, resolved by the same Prowlarr priority. Left
off, every entry that carried a release keeps its own row, which is what
makes filtered results visible again.

Identity handling is defensive about partial payloads: a result that
cannot be identified is never dropped or merged, and collapse only
merges on a strong identifier (guid/downloadUrl/magnetUrl/infoUrl)
because merging on title alone would discard genuinely different
releases that share a name.

Fixes #1137

Co-authored-by: delize <4028612+delize@users.noreply.github.com>
2026-07-28 01:06:39 -04:00
Adam Vigneaux
0d02c6db47 Add qBittorrent API key authentication (#1143)
[qBittorrent

5.2.0](https://www.qbittorrent.org/news#sun-may-03rd-2026---qbittorrent-v5.2.0-release)
(May 2026) added support for API key-based authentication in addition to
the existing username/password-based authentication.

This commit adds support for qBittorrent API key authentication to
Shelfmark, configurable via environment variable or settings UI. If an
API key is set at the same time as the username/password, API key will
be preferred for authentication.

Requires `qbittorrent-api` 2026.5.3, the version that added the
`api_key` argument, or newer. `403 Forbidden` responses are not retried
with API key authentication because a retry has no chance of succeeding.

Tested end-to-end with my live qBittorrent 5.2.3 instance (WebAPI
v2.15.1).

<img width="785" height="616" alt="image"
src="https://github.com/user-attachments/assets/8064e10e-9a7f-49f0-804e-4c441d23a1fc"
/>
2026-07-28 00:54:19 -04:00
CaliBrain
7fdaf3f67d Add Audiobook support to IRC (#1136) 2026-07-24 18:34:12 -04:00
CaliBrain
81a057c8a0 Try and fix OIDC (#1127) 2026-07-21 15:43:54 -04:00
CaliBrain
404e8cc5c5 path timeouts (#1103)
- Add configurable completed-path wait for external clients : - Add a
configurable Advanced setting, DOWNLOAD_CLIENT_COMPLETED_PATH_TIMEOUT,
for how long Shelfmark waits after a torrent or usenet client reports
completion before treating the completed path as missing. - Keep the
default at the existing 60-second grace period, with a maximum of 3600
seconds
- Add e2e testing
- Add e2e testing

Should fix #861
2026-07-06 01:51:58 -04:00
Alex Schittko
40d6a179b7 feat: native WireGuard VPN egress mode (USING_WIREGUARD) (#1097)
Add an opt-in WireGuard egress path alongside the existing Tor mode

---------

Co-authored-by: CaliBrain <calibrain@l4n.xyz>
2026-07-06 01:51:18 -04:00
CaliBrain
c64c2d374a Make IRC less spammy and require a bot name for conversatons (#1065)
First step towards fixing the friction created by shelfmark in #997
2026-06-14 01:40:09 -04:00
spindrift
0633cfde87 Add content_type=combined URL parameter and FORCE_COMBINED_SEARCH user setting (#1058)
Adds support for `content_type=combined` in URL search parameters,
letting users force combined-mode searches via a bookmarkable link
rather than relying on the last-used preference from localStorage.

The override is applied only in Universal mode and only when combined
mode is actually available (universal enabled, `show_combined_selector`
on, neither content type blocked by policy). Otherwise, it's silently
ignored, consistent with how `content_type=ebook`/`audiobook` already
behave outside Universal.

Existing `content_type=ebook`/`audiobook` URLs now also force combined
mode off, so the URL is authoritative regardless of prior preference.

Also adds a per-user `FORCE_COMBINED_SEARCH` setting that locks combined
mode on whenever it's available.

URL `content_type=ebook`/`audiobook` overrides are also superseded by
force-combined for the same reason: the search bar wouldn't let users
switch back, so honoring the URL param would leave them in a state they
couldn't escape from.
2026-06-14 00:09:30 -04:00
Alex
9b8402c9a7 Add DISABLE_LOCAL_AUTH env variable (#962)
Adds a new env var to disable local auth entirely when using OIDC
authentication

Fixes #922 #834
2026-05-08 22:11:14 +01:00
Alex
3554d01c81 Change path default for audiobooks + description fixes (#933) 2026-04-30 18:20:05 +01:00
Alex
e35b4c47a7 Direct source refactor (#895)
- Updated mirror selection
- Removed built-in mirror options, users must provide their own
configurations
- Set Universal search to default, added ability to disable direct
source
- Updated documentation
- Updated makefile
2026-04-15 18:50:13 +01:00
Alex
87d5f127d6 Add prek + pytest-cov (#873) 2026-04-12 12:39:15 +01:00
Alex
41c4aa1d72 Updated permissions model and non-root support (#871)
- Adds a non-root startup path at user 1000:1000 - skips privilege
escalation and ownership checks. Works e.g. for kubernetes deployments
(user 1000:1000 and runAsNonRoot enabled).
- Remove startup check/chown commands for user-owned folders. Checks can
be done with a "Test destination" button in settings which performs a
test write. Users are responsible for fixing their own permissions.
- Update docs
2026-04-12 08:22:32 +01:00
Alex
b3b8f34a13 Fix JSON script blocking behavior + tests (#862)
Fixes #859
2026-04-10 20:46:07 +01:00
Alex
8f949a73d5 Remove audible provider (#778) 2026-03-18 18:29:13 +00:00
cadric
3295be82a7 Add Audible metadata provider via Audimeta (#762)
Closes #515

  ## Summary

This adds a new `audible` metadata provider backed by the Audimeta API.

  The provider supports:
  - Audible/Audimeta metadata lookup without authentication
- region selection (`us`, `ca`, `uk`, `au`, `fr`, `de`, `jp`, `it`,
`in`, `es`, `br`)
  - ASIN book lookup
  - ISBN lookup with fallback search
  - series suggestions and series-order browsing
- richer audiobook metadata such as narrators, runtime, rating,
subtitle, cover, publisher, and series info
- configurable Audimeta base URL, timeout, cache usage, default sort,
and unreleased filtering

  ## Notes

  A few Audimeta-specific integration details were needed:
- send a meaningful `User-Agent`, otherwise Audimeta rejects requests
with `403`
  - send the `cache` parameter in the format Audimeta expects
- use `keywords` for general search instead of `query`, which gave
poor/irrelevant results for title-style
  searches

  ## Validation

  Tested locally with:
  - `python -m py_compile shelfmark/metadata_providers/audible.py`
  - `python -m pytest tests/metadata/test_audible.py -v`
- `python -m pytest
tests/metadata/test_metadata_provider_capabilities.py -v`

  Also verified manually in a Podman test container:
  - searching for `Discount Dan` returns Audible title `B0DXLXRNGG`
  - book details and series metadata load correctly

  ## Scope

This PR intentionally keeps the change localized to the provider layer
and docs:
  - new Audible provider
  - provider registration
  - provider docs
  - generated environment variable docs
2026-03-15 10:09:24 +00:00
Alex
3d72f9e258 Various requested small features (#741)
- Added torrent removal option
- Pass Prowlarr seedtimes to download clients (excluding rTorrent)
- Split default release source option by content type
- Split download to browser option by content type
- Add "hide links" option
2026-03-12 17:36:07 +00:00
Alex
c59ea46540 Frontend update + Misc fixes (#735)
- Updated frontend CSS to Tailwind v4
- Reverted socket IO origin restriction
- Fixed search queries not persisting after auth redirect
- Move advanced search options to left UI selector
- Unlock IRC source to be used for audiobook content_type
- Tweaked security settings env var syncing to be prioritised
- Fix AA "all languages" query generation
- Added language-free AA query as second fallback in case of no results
- Testing moving SeleniumBase scratch files to /tmp via symlink
- Added enhanced logging for activity dismissals and other events
- Removed iFrame restrictions
2026-03-11 18:16:34 +00:00
Alex
ba92ad90bc Refine UI and adjust content type settings (#705)
- Tweak manual search toggle position
- Refinements to the Hardcover list dropdown behavior
- Hide the content type dropdown when a content type is blocked for a
user
- Fixes to Hardcover author parsing to strip out initialed names
- Remove `env_supported=false` for security config options.
2026-03-05 16:24:03 +00:00
Alex
71900e00db Feature: Hardcover list search (#694)
- Adds the functionality to search Hardcover lists, either public lists
or user's private lists
- Paste a list URL into the search box to view results
- Select a specific list from user's collection from advanced fields
dropdown
- Fixes content_type parameter in URL search query to use book/audiobook
2026-03-03 21:53:43 +00:00
Alex
014fc38b48 Patch: OIDC polish (#636)
- Added two env vars for OIDC login: 
- HIDE_LOCAL_AUTH - Remove the "password" option on login page when OIDC
enabled
  - OIDC_AUTO_REDIRECT - Immediately launch OIDC provider page
- Improved UX for initial OIDC setup, including creating a local admin
user
- Added callback URL label to OIDC setup page
- Fix Qbittorrent save path bug
2026-02-21 11:51:11 +00:00
Alex
fdd46852f2 Add new docs (#633) 2026-02-20 18:07:43 +00:00
Alex
554f5fcbe7 Patch: Various feature additions (#625)
- Add admin config for self-settings options visibility. Remove delivery
preferences or notifications from the view.
- Add option to use Booklore's Bookdrop API destination instead of a
specific library
- Add download path options for all torrent clients
2026-02-20 09:53:47 +00:00
Alex
68608b6162 Feature: Multi-user request system (#615)
- Adds a comprehensive multi-user request system to the existing
download flow
- Request configuration is policy based. Configure global settings for
content type, or narrow down policy for specific sources (E.g. allow
direct downloads, set prowlarr to request only, block IRC completely,
etc).
- Global policy configuration and per-user overrides for tailored
configs
- Replaced downloads sidebar with ActivitySidebar, combining active
downloads with requests. Admin management of user requests is done here,
and admins have view of downloads from all users. Sidebar can now be
pinned.
- Request either a standard book or a specific release. Release-requests
are used if you permit one source differently than the other. On
book-level requests, admins pick the specific file to be attached to the
fulfilled request.
- Users can request books with a note

This is WIP so some features are still not complete (notifications, more
automatic release selection, among others).
2026-02-14 11:08:20 +00:00
arjunsrinivasan1997
a7064939ce feat: Add tag support to qBittorrent (#610)
Added support for adding tag(s) to torrents sent to qBittorrent via
shelfmark.
![Screenshot 2026-02-11 at 3 58
20 AM](https://github.com/user-attachments/assets/aa9b440a-27fd-4166-953b-31f5179688a3)
![Screenshot 2026-02-11 at 3 53
14 AM](https://github.com/user-attachments/assets/15084b44-9a68-493c-85e8-328c92206c85)
2026-02-12 14:52:34 +00:00
Alex
a560089ce3 Patch: Script improvements + bug fixes (#591)
- Add new booklore API file formats
- Renamed cookie for better login persistence with reverse proxy
- Updated fs.py to try hardlink before atomic move from tmp dir
- Fix transmission URL parsing 
- Fix scenario where file processing of huge files starves the
healthcheck
- Large enhancements to custom scripting, including passing JSON
download info, more consistent activation across output types,
decoupling from staging behavior, and added full documentation.
2026-02-06 13:51:23 +00:00
Alex
f84fb082ad Fix: AA mirror behavior (#589)
- Refreshed available AA URLs
- Fixed potential redirect from AA itself causing mirror cache errors
- Added fully customizable mirror list in UI
- Segmented rotation behavior to Auto mode only

Fixes #588
2026-02-06 10:04:31 +00:00
Alex
b10458a48b Patch: Migrate bypasser to pure CDP + Misc fixes (#575)
Bypasser:
- Refactored internal bypasser logic to use SeleniumBase Pure CDP mode,
removed chromedriver dependencies and UC code.
- Added dedicated threading for internal bypasser functions, fixes any
potential asyncio CPU spike behavior
- Fixed WebGL issue with Chromium 144. Reverted 1.0.3 hotfix and updated
to latest Chromium

Misc: 
- Added M4A color mapping
- Fix frontend language filtering with multi-language releases
- Added "days" age for usenet/torrent releases
- Improved entrypoint chown efficiency
- Added `ONBOARDING` env variable, default true
2026-02-02 20:32:19 +00:00
Alex
e5ccabe1ef Patch: Various additions (#564)
- Added rich Prowlarr search results for whitelisted indexers
- Added torznab query for whitelisted indexers
- Added flags for all Prowlarr indexers
- Added completed external download retry mechanism and "locating" state
- Added client side preference storage of Book/Audiobook search
preference
- Fixed reverse proxy base URL in edge cases
- Added gevent locking for I/O operations, keeps healthcheck alive on
intensive processing operations
- Added M4A supported audiobook option
- Improved file transfer counting and logging with hardlink fallback
warnings
- Fixed proxy auth header for REMOTE_USER scenario
- Dependency tweak for internal bypasser
2026-01-31 12:53:11 +00:00
Khakis
0a7785a333 Docs: Subpath reverse proxy configuration (#542)
## Summary

This PR updates the reverse proxy documentation with comprehensive
configuration examples for subpath deployments, addressing several
issues discovered when running Shelfmark behind nginx at a subpath like
`/shelfmark/`.

## Changes

- **Root path setup**: Added complete nginx server block example
- **Subpath setup without auth**: Complete nginx configuration with all
necessary workarounds
- **Subpath setup with Authelia**: Full example including Authelia
snippets and Shelfmark proxy auth settings
- **Known issues section**: Documents the frontend bugs that require
workarounds

## Issues Addressed

The current documentation's simple example doesn't work for subpath
deployments because:

1. **Socket.IO connects to root**: Frontend connects to `/socket.io/`
instead of `/shelfmark/socket.io/`
2. **API calls use root path**: Cover images request `/api/` instead of
`/shelfmark/api/`
3. **Logo uses root path**: Requested from `/logo.png` instead of
`/shelfmark/logo.png`
4. **Socket.IO backend path**: Always at `/socket.io/` regardless of
`URL_BASE` setting

## Testing

Tested with:
- Nginx reverse proxy
- Authelia authentication proxy
- `URL_BASE=/shelfmark/` configuration
- WebSocket connections working
- Cover images loading
- Proxy authentication with admin group restrictions

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 20:03:35 +00:00
Alex
3be99effe4 Base url additions and bug fixes (#519)
- Base URL option in settings for reverse proxy setups
- Fix NZB downloads not deleting on completion
- Fix handling for audiobook files over 100+ parts
- Fix prowlarr search timeout 
- Fix prowlarr categorisation for expanded searches
2026-01-23 13:03:02 +00:00
Alex
fd74021594 File processing refactor and Booklore upload support (#474)
- Added new book output option **upload to Booklore**, available in
download settings
- Got annoyed at my messy processing code while implementing Booklore so
refactored the whole thing
- Full black box file processing testing with randomised configuration
- Deluge: Connect via WebUI auth for simplified setup
- Added env vars documentation, auto generated via script, and unlocked
most settings to be used as env vars
2026-01-16 14:45:00 +00:00
Alex
a0079c5a7f Added onboarding + release fixes (#433) 2026-01-13 11:25:12 +00:00
Alex
afeae46821 Rename to Shelfmark and IRC adjustments (#415) 2026-01-11 19:38:38 +00:00
Alex
a4173eafcb Restructure + abstraction, plugin system, settings UI, universal search mode (#351)
Key changes:   

| Category | Lines | What it is |

|--------------------------|--------|----------------------------------------------------------------------|
| Docs | ~2,100 | plugin-settings.md, release-sources-plugin-guide.md,
provider README |
| Settings UI | ~1,650 | Modal, sidebar, field components (TextField,
SelectField, etc.) |
| ReleaseModal | ~1,200 | Universal mode release picker UI |
| Metadata Providers | ~2,100 | Hardcover + OpenLibrary + base classes |
| Core Infrastructure | ~2,150 | Cache decorator, queue, image cache,
models, config |
| main.py | ~1,570 | Flask routes (replaces old app.py but bigger) |
| Orchestrator | ~590 | Download queue management |
| Config/Settings Registry | ~1,400 | Backend settings system |
| Frontend Hooks | ~750 | useSettings, useSearch, useDownloadTracking,
etc. |
| Other Frontend | ~500 | BookGetButton, ReleaseCell, utils |
| Release Sources base | ~320 | Plugin interfaces |
2025-12-22 12:13:11 -05:00