Skip to content

feat: narrator, series and bitrate columns for MyAnonamouse results - #1390

Merged
calibrain merged 6 commits into
calibrain:mainfrom
adman234:feature/mam-enrichment
Sep 25, 2026
Merged

calibrain merged 6 commits into
calibrain:mainfrom
adman234:feature/mam-enrichment

Conversation

@adman234

Copy link
Copy Markdown
Contributor

feat: narrator, series and bitrate columns for MyAnonamouse results

Addresses #605 and #934 (narrator in the release list).

Problem

Both requests were closed because the narrator isn't in Torznab results, which is correct. Prowlarr's MyAnonamouse indexer reads author_info but drops the narrator_info and series_info MAM returns next to it, and neither Prowlarr's ReleaseInfo nor the Torznab output has a field for them. Shelfmark's size tooltip already looks for a narrator Torznab attribute, but nothing ever sends one.

When a book has several narrations, choosing one means going back and forth between Shelfmark and the tracker.

Approach

Optional, opt-in enrichment using the user's own MAM session (mam_id), the same approach AudioBookRequest's MAM indexer uses:

  1. After the Prowlarr search, releases whose info URL is …myanonamouse.net/t/<id> are collected.
  2. Shelfmark sends the same query text to MAM's JSON search (/tor/js/loadSearchJSONbasic.php, normally one request, at most 3), and matches torrents back to Prowlarr's results by torrent ID. The MAM origin is taken from the result URL, so a custom Prowlarr MAM base URL is respected, and the cookie is only ever sent to *.myanonamouse.net.
  3. Adds extra.narrator, extra.series (The Sun Eater #1) and extra.bitrate / extra.bitrate_value. MAM has no bitrate field, so it's parsed from the uploader's free-text tags (64 kbps) and some releases won't have one.

Lookups are cached per torrent for an hour, respect the existing Prowlarr search deadline, go through the configured proxy (get_proxies), and never fail the search: a 403, timeout or bad JSON is logged and the list renders without the extra details.

Changes

  • release_sources/prowlarr/mam.py (new): small MAM client (search, and get_username for the test button), narrator_info / series_info / tags parsing, cached best-effort lookup_torrent_details(). A 403 includes MAM's reply and a note about the IP/ASN lock.
  • release_sources/prowlarr/source.py: enrichment after the result loop. Series, Narrator and Bitrate columns only when a MAM ID is configured, since otherwise they would be empty for every row. A Torznab bitrate attribute from other indexers is also mapped to extra.bitrate.
  • release_sources/prowlarr/settings.py: "MyAnonamouse Enrichment" section with PROWLARR_MAM_ID (password field, env-overridable like every setting) and a Test MAM Session button.
  • release_sources/__init__.py: ColumnSchema gains optional setting_key and content_types. The new apply_column_visibility() drops gated columns and their grid tracks. Neither field is serialized.
  • main.py: /api/releases applies apply_column_visibility() with the request's content type and the user's effective settings.
  • config/settings.py / users_settings.py: Search Mode › "Release List Columns" with SHOW_SERIES_COLUMN, SHOW_NARRATOR_COLUMN and SHOW_BITRATE_COLUMN, all default on and user-overridable. Narrator and bitrate are audiobook-only, series shows for both. AudiobookBay's existing bitrate column now follows the bitrate toggle.
  • Frontend: text cells truncate with a hover title; the mobile info line wraps and skips empty text/number cells so blank optional columns don't leave orphan · separators; the size tooltip no longer lists Bitrate twice.
  • Docs: new docs/myanonamouse-enrichment.md (linked from the index), and a regenerated environment-variables.md. The regeneration also picked up a few pre-existing drifts from main (the Libgen section, AA_DEFAULT_SORT default, a duplicate BOOK_LANGUAGE row). I can drop those if you'd rather keep this diff focused.

⚠️ MAM sessions are IP/ASN-locked

MyAnonamouse locks each session to one IP or ASN. Reusing the session Prowlarr (or a seedbox script) uses will often 403. A separate MAM session for Shelfmark will likely be needed when Shelfmark reaches MAM from a different IP (another host, a VPN container, or a proxy in Shelfmark's Network settings), or when the existing session is ASN-locked to another network. The setting's description, the error message and the new doc all say so.

Testing

  • tests/prowlarr/test_mam_enrichment.py (new, 19 tests): parsing (narrator dedupe, multiple series, missing numbers, malformed JSON, tag bitrate), lookup (stops once all IDs are found, cache, 403 and connection errors return empty, expired deadline skips the request), only MAM releases enriched, Torznab bitrate mapping, column config with and without a MAM ID for audiobook and ebook, toggles, grid-track removal, and gates not serialized.
  • tests/core/test_admin_users_api.py: the curated search-preference key list now includes the three toggles.
  • Full pytest -m "not integration and not e2e" compared with an upstream main worktree on the same machine: no new failures. The remaining ~115 failures on both are Windows-only (tor/entrypoint shell tests, path separators).
  • ruff check / ruff format --check / basedpyright (0 errors) / vulture on touched files; frontend tsc --noEmit, oxlint, oxfmt --check, vitest (201 passed).
  • Manually verified with a real MAM account on a Docker build of this branch: the test button, then narrator, series and bitrate on MyAnonamouse audiobook results.

No behavior change unless PROWLARR_MAM_ID is set, apart from the bitrate toggle on AudiobookBay (default on, same as today).

🤖 Generated with Claude Code

adman234 and others added 6 commits September 24, 2026 12:37
Prowlarr's MyAnonamouse indexer drops the narrator and series MAM returns,
and Torznab has no field for them. With a MAM session ID (PROWLARR_MAM_ID,
settable in the Prowlarr settings or as an env var) Shelfmark reruns the
Prowlarr query against MAM's JSON search API and matches torrents back by
ID, filling extra.narrator, extra.series and extra.bitrate (parsed from the
uploader's tags). Lookups are cached for an hour, stay inside the Prowlarr
search budget, and never fail the search.

New Search Mode toggles SHOW_SERIES_COLUMN, SHOW_NARRATOR_COLUMN and
SHOW_BITRATE_COLUMN (user-overridable) control the columns. Columns can now
declare a setting_key and content_types, applied by apply_column_visibility()
in /api/releases; narrator and bitrate are audiobook-only. AudiobookBay's
bitrate column follows the same toggle, and a Torznab bitrate attribute from
other indexers is used when present.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MAM locks each session to one IP or ASN, so reusing Prowlarr's session
often 403s from Shelfmark. Say so in the MAM Session ID setting, the
generated env docs, and a new docs/myanonamouse-enrichment.md guide.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ides

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@calibrain
calibrain merged commit a05e002 into calibrain:main Sep 25, 2026
9 checks passed
@calibrain

Copy link
Copy Markdown
Owner

Thank you !

calibrain added a commit that referenced this pull request Sep 26, 2026
#1399)

Follow-up to #1390.

- The origin came from each result's infoUrl, matched by a regex that
did
  not check the host, and the mam_id cookie had no domain. Any Prowlarr
  indexer returning a URL like https://evil.example?myanonamouse.net/t/1
  sent the session ID to evil.example. Requests now always go to
https://www.myanonamouse.net (Prowlarr's only MAM URL), with the cookie
  as a header and redirects off. Only results from Prowlarr's
  MyAnonamouse indexer are looked up, and their URLs must be on
  myanonamouse.net.
- The lookup searched every category and read one page, so for common
  titles most of Prowlarr's results were missed (a "Dune" audiobook
  search: 56 audiobooks on the first 100 of 328 matches). It now reruns
  Prowlarr's exact search: the same query clean-up, the MAM main
  categories behind the Torznab categories searched (13/15/16 for
  audiobooks, 14 for e-books, all once expanded), and the MAM indexer's
  own search type, search-in options and languages. Further pages are
  read while IDs are missing, page 1 of every title first, at most 4
  requests per search.
- Failed requests back off for 1, 2, 4 ... up to 30 minutes. The 10th
  consecutive failure stops enrichment until Test MAM Session passes,
  the session ID changes, or Shelfmark restarts.
- The detail cache prunes expired entries instead of growing for as long
  as Shelfmark runs.
doonga pushed a commit to greyrock-labs/home-ops that referenced this pull request Sep 26, 2026
…v1.4.0) (#283)

This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [ghcr.io/calibrain/shelfmark](https://github.com/calibrain/shelfmark) | minor | `v1.3.15` → `v1.4.0` |

---

### Release Notes

<details>
<summary>calibrain/shelfmark (ghcr.io/calibrain/shelfmark)</summary>

### [`v1.4.0`](https://github.com/calibrain/shelfmark/releases/tag/v1.4.0)

[Compare Source](calibrain/shelfmark@v1.3.15...v1.4.0)

##### What's Changed

- build(deps): bump the python-deps group with 2 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;1302](calibrain/shelfmark#1302)
- fix: keep polling queued Real-Debrid torrents by [@&#8203;mvanhorn](https://github.com/mvanhorn) in [#&#8203;1303](calibrain/shelfmark#1303)
- fix(bypass): keep Anna's Archive's aa\_ddg\_check so clearance replays by [@&#8203;jfmlima](https://github.com/jfmlima) in [#&#8203;1305](calibrain/shelfmark#1305)
- fix(postprocess): attach unmatched chaptered audio files to existing book group ([#&#8203;1176](calibrain/shelfmark#1176)) by [@&#8203;amasen02](https://github.com/amasen02) in [#&#8203;1309](calibrain/shelfmark#1309)
- build(deps): bump the gh-actions group with 3 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;1310](calibrain/shelfmark#1310)
- build(deps): bump the npm-deps group in /src/frontend with 4 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;1308](calibrain/shelfmark#1308)
- build(deps): bump python from `cae66f2` to `cad9a2c` by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;1307](calibrain/shelfmark#1307)
- build(deps): bump the docker-base-image-digests group with 2 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;1306](calibrain/shelfmark#1306)
- Deep-link Search By mode via URL hash by [@&#8203;nfvelten](https://github.com/nfvelten) in [#&#8203;1311](calibrain/shelfmark#1311)
- build(deps): bump the python-deps group across 1 directory with 4 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;1327](calibrain/shelfmark#1327)
- Keep default filters out of the URL hash by [@&#8203;nfvelten](https://github.com/nfvelten) in [#&#8203;1314](calibrain/shelfmark#1314)
- feat(download): add Blackhole torrent handoff by [@&#8203;atirna](https://github.com/atirna) in [#&#8203;1312](calibrain/shelfmark#1312)
- feat(naming): add {FirstAuthor} template token by [@&#8203;viniciuspx](https://github.com/viniciuspx) in [#&#8203;1322](calibrain/shelfmark#1322)
- fix: share rotating log file handlers by [@&#8203;eikopf](https://github.com/eikopf) in [#&#8203;1316](calibrain/shelfmark#1316)
- fix(prowlarr): skip indexers in Prowlarr failure back-off by [@&#8203;jfmlima](https://github.com/jfmlima) in [#&#8203;1324](calibrain/shelfmark#1324)
- fix: prevent Anna's Archive download countdown resets by preserving browser sessions by [@&#8203;broglea](https://github.com/broglea) in [#&#8203;1325](calibrain/shelfmark#1325)
- build(deps): bump the docker-base-image-digests group with 2 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;1328](calibrain/shelfmark#1328)
- build(deps-dev): bump the npm-deps group in /src/frontend with 4 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;1329](calibrain/shelfmark#1329)
- build(deps-dev): bump vitest from 4.1.11 to 5.0.0 in /src/frontend by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;1330](calibrain/shelfmark#1330)
- fix(irc): search by surname, and rank the answer by author ([#&#8203;1331](calibrain/shelfmark#1331)) by [@&#8203;Kukkerem](https://github.com/Kukkerem) in [#&#8203;1332](calibrain/shelfmark#1332)
- fix(irc): rank a surname-only result as partial, not wrong ([#&#8203;1332](calibrain/shelfmark#1332)) by [@&#8203;calibrain](https://github.com/calibrain) in [#&#8203;1334](calibrain/shelfmark#1334)
- feat(sources): add Libgen as a direct catalogue search source by [@&#8203;klaidliadon](https://github.com/klaidliadon) in [#&#8203;1326](calibrain/shelfmark#1326)
- refactor: make direct download provider-driven by [@&#8203;TomJansen](https://github.com/TomJansen) in [#&#8203;1337](calibrain/shelfmark#1337)
- fix(sources): restore Direct Download search errors and language matches by [@&#8203;calibrain](https://github.com/calibrain) in [#&#8203;1339](calibrain/shelfmark#1339)
- fix(sources): send a Referer when fetching libgen ads.php pages by [@&#8203;klaidliadon](https://github.com/klaidliadon) in [#&#8203;1340](calibrain/shelfmark#1340)
- Feature: Add Download counts to search result displays and Download sidebar by [@&#8203;RoninTech](https://github.com/RoninTech) in [#&#8203;1336](calibrain/shelfmark#1336)
- Extract archives when zip/rar are enabled as supported formats by [@&#8203;funkypenguin](https://github.com/funkypenguin) in [#&#8203;1343](calibrain/shelfmark#1343)
- fix(download): complete consumed Blackhole handoffs by [@&#8203;atirna](https://github.com/atirna) in [#&#8203;1345](calibrain/shelfmark#1345)
- Add configurable word separator for naming templates by [@&#8203;viniciuspx](https://github.com/viniciuspx) in [#&#8203;1333](calibrain/shelfmark#1333)
- build(deps): bump the python-deps group across 1 directory with 5 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;1344](calibrain/shelfmark#1344)
- fix: unbreak main and follow up on the Blackhole handoff review by [@&#8203;calibrain](https://github.com/calibrain) in [#&#8203;1346](calibrain/shelfmark#1346)
- build(deps): bump python-socketio from 5.16.4 to 5.17.0 in the python-deps group by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;1347](calibrain/shelfmark#1347)
- Added the ability to sort direct search results by Most downloads by [@&#8203;RoninTech](https://github.com/RoninTech) in [#&#8203;1351](calibrain/shelfmark#1351)
- feat: Add TorBox client support and settings integration by [@&#8203;marcelorodrigo](https://github.com/marcelorodrigo) in [#&#8203;1342](calibrain/shelfmark#1342)
- refactor: extract the per-source release search out of /api/releases by [@&#8203;splitsec2](https://github.com/splitsec2) in [#&#8203;1355](calibrain/shelfmark#1355)
- build(deps): bump the docker-base-image-digests group with 2 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;1348](calibrain/shelfmark#1348)
- build(deps): bump the npm-deps group in /src/frontend with 9 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;1349](calibrain/shelfmark#1349)
- build(deps): bump the gh-actions group with 6 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;1350](calibrain/shelfmark#1350)
- feat(auth): provision proxy users as non-admin once an admin exists by [@&#8203;splitsec2](https://github.com/splitsec2) in [#&#8203;1356](calibrain/shelfmark#1356)
- fix(users): apply user updates only after the payload validates by [@&#8203;splitsec2](https://github.com/splitsec2) in [#&#8203;1360](calibrain/shelfmark#1360)
- fix(download): default is\_admin to False in the request policy guard by [@&#8203;splitsec2](https://github.com/splitsec2) in [#&#8203;1358](calibrain/shelfmark#1358)
- fix(oidc): reject backslash paths in the return\_to sanitizer by [@&#8203;splitsec2](https://github.com/splitsec2) in [#&#8203;1359](calibrain/shelfmark#1359)
- fix(queue): don't stamp CANCELLED over a finished download by [@&#8203;splitsec2](https://github.com/splitsec2) in [#&#8203;1361](calibrain/shelfmark#1361)
- fix(download): check task ownership before serving queued files by [@&#8203;splitsec2](https://github.com/splitsec2) in [#&#8203;1357](calibrain/shelfmark#1357)
- Feature: Show the AA search result stats by [@&#8203;RoninTech](https://github.com/RoninTech) in [#&#8203;1362](calibrain/shelfmark#1362)
- fix(requests): reject non-object items in the batch endpoint by [@&#8203;splitsec2](https://github.com/splitsec2) in [#&#8203;1369](calibrain/shelfmark#1369)
- fix(http): keep the host of a protocol-relative download link by [@&#8203;splitsec2](https://github.com/splitsec2) in [#&#8203;1368](calibrain/shelfmark#1368)
- fix(googlebooks): page by the capped size, not the raw limit by [@&#8203;splitsec2](https://github.com/splitsec2) in [#&#8203;1370](calibrain/shelfmark#1370)
- fix(deluge): send seeding ratio limit under Deluge's own keys by [@&#8203;splitsec2](https://github.com/splitsec2) in [#&#8203;1367](calibrain/shelfmark#1367)
- feat(auth): static API\_KEY (env) accepted as Bearer or X-Api-Key, cookie or key by [@&#8203;gavinmcfall](https://github.com/gavinmcfall) in [#&#8203;1366](calibrain/shelfmark#1366)
- fix(auth): rename the API\_KEY env var to SHELFMARK\_API\_KEY by [@&#8203;calibrain](https://github.com/calibrain) in [#&#8203;1374](calibrain/shelfmark#1374)
- fix: bypass recordings, welib wrong-md5 links, footer build sha ([#&#8203;1364](calibrain/shelfmark#1364)) by [@&#8203;calibrain](https://github.com/calibrain) in [#&#8203;1373](calibrain/shelfmark#1373)
- build(deps): bump the python-deps group with 4 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;1375](calibrain/shelfmark#1375)
- ci: debounce dev image builds instead of building nightly by [@&#8203;calibrain](https://github.com/calibrain) in [#&#8203;1376](calibrain/shelfmark#1376)
- fix(download): stream a completed book instead of buffering it in RAM - lowering memory needs significantly by [@&#8203;splitsec2](https://github.com/splitsec2) in [#&#8203;1378](calibrain/shelfmark#1378)
- perf(docker): keep the heavy build layers cacheable across builds - save 11minutes per build by [@&#8203;splitsec2](https://github.com/splitsec2) in [#&#8203;1379](calibrain/shelfmark#1379)
- test(auth): stop proxy provisioning tests depending on run order by [@&#8203;splitsec2](https://github.com/splitsec2) in [#&#8203;1381](calibrain/shelfmark#1381)
- feat(search): add a configurable default content type by [@&#8203;splitsec2](https://github.com/splitsec2) in [#&#8203;1371](calibrain/shelfmark#1371)
- feat(library): mark search results already in a Calibre library by [@&#8203;splitsec2](https://github.com/splitsec2) in [#&#8203;1377](calibrain/shelfmark#1377)
- Default to english when no lang is slected by [@&#8203;calibrain](https://github.com/calibrain) in [#&#8203;1396](calibrain/shelfmark#1396)
- fix(auth): fail closed when auth prerequisites are missing ([#&#8203;1387](calibrain/shelfmark#1387)) by [@&#8203;calibrain](https://github.com/calibrain) in [#&#8203;1397](calibrain/shelfmark#1397)
- build(deps): bump the gh-actions group with 4 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;1395](calibrain/shelfmark#1395)
- fix(audiobookbay): reuse the resolved magnet when retrying ([#&#8203;1388](calibrain/shelfmark#1388)) by [@&#8203;calibrain](https://github.com/calibrain) in [#&#8203;1398](calibrain/shelfmark#1398)
- build(deps): bump the npm-deps group in /src/frontend with 7 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;1394](calibrain/shelfmark#1394)
- build(deps): bump seleniumbase from 4.54.9 to 4.54.10 in the python-deps group by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;1382](calibrain/shelfmark#1382)
- feat: narrator, series and bitrate columns for MyAnonamouse results by [@&#8203;adman234](https://github.com/adman234) in [#&#8203;1390](calibrain/shelfmark#1390)
- build(deps): bump astral-sh/uv from 0.12.16 to 0.12.19 in the docker-base-image-digests group across 1 directory by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;1392](calibrain/shelfmark#1392)
- fix(mam): keep the session ID on MAM and rerun Prowlarr's exact search by [@&#8203;calibrain](https://github.com/calibrain) in [#&#8203;1399](calibrain/shelfmark#1399)
- fix(ui): contain the result count added in [#&#8203;1362](calibrain/shelfmark#1362) by [@&#8203;calibrain](https://github.com/calibrain) in [#&#8203;1363](calibrain/shelfmark#1363)

##### New Contributors

- [@&#8203;amasen02](https://github.com/amasen02) made their first contribution in [#&#8203;1309](calibrain/shelfmark#1309)
- [@&#8203;atirna](https://github.com/atirna) made their first contribution in [#&#8203;1312](calibrain/shelfmark#1312)
- [@&#8203;viniciuspx](https://github.com/viniciuspx) made their first contribution in [#&#8203;1322](calibrain/shelfmark#1322)
- [@&#8203;eikopf](https://github.com/eikopf) made their first contribution in [#&#8203;1316](calibrain/shelfmark#1316)
- [@&#8203;broglea](https://github.com/broglea) made their first contribution in [#&#8203;1325](calibrain/shelfmark#1325)
- [@&#8203;klaidliadon](https://github.com/klaidliadon) made their first contribution in [#&#8203;1326](calibrain/shelfmark#1326)
- [@&#8203;TomJansen](https://github.com/TomJansen) made their first contribution in [#&#8203;1337](calibrain/shelfmark#1337)
- [@&#8203;RoninTech](https://github.com/RoninTech) made their first contribution in [#&#8203;1336](calibrain/shelfmark#1336)
- [@&#8203;marcelorodrigo](https://github.com/marcelorodrigo) made their first contribution in [#&#8203;1342](calibrain/shelfmark#1342)
- [@&#8203;splitsec2](https://github.com/splitsec2) made their first contribution in [#&#8203;1355](calibrain/shelfmark#1355)
- [@&#8203;gavinmcfall](https://github.com/gavinmcfall) made their first contribution in [#&#8203;1366](calibrain/shelfmark#1366)

**Full Changelog**: <calibrain/shelfmark@v1.3.15...v1.4.0>

</details>

---

### Configuration

📅 **Schedule**: (in timezone America/New_York)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these updates again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate CLI](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMDUuMiIsInVwZGF0ZWRJblZlciI6IjQ0LjEwNS4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJyZW5vdmF0ZS9jb250YWluZXIiLCJ0eXBlL21pbm9yIl19-->

Reviewed-on: https://git.greyrock.io/todd/home-ops/pulls/283
@mikaeltarquin

Copy link
Copy Markdown

Can this now be used to add a {Narrator} text field to the download path? I am trying to get audiobooks to download to “/{Author}/{Title} {{Narrator}}/{Title}” so that I can have multiple versions of the same book without having to manually rename folders each time.

@adman234

Copy link
Copy Markdown
Contributor Author

Can this now be used to add a {Narrator} text field to the download path? I am trying to get audiobooks to download to “/{Author}/{Title} {{Narrator}}/{Title}” so that I can have multiple versions of the same book without having to manually rename folders each time.

Good idea, I've created a new PR for this change.

#1407

calibrain pushed a commit that referenced this pull request Oct 3, 2026
…#1407)

## Human-Written explanation:

Following up on previous PR to use MAM ID to get nararrator and series
to show up in search, this PR will allow the series and nararrator
fields to be added to the path when saving an audiobook.

I have tested my ghcr.io image on my instance and it seemed to work
properly.

All the text below is written by Claude.

# feat(naming): {Narrator} placeholder and MyAnonamouse series fallback

Follow-up to #1390 and #1399. Related to #605 and #934 (narrator
support).

## Problem

Several narrations of the same audiobook currently land in the same
folder, because nothing in the path template tells them apart. Keeping
more than one version means renaming folders by hand after every
download.

Since #1390, MyAnonamouse results carry the release's narrator and
series in `extra`, but the naming templates can't use them: there is no
`{Narrator}` placeholder, and `{Series}` / `{SeriesPosition}` only come
from the metadata provider.

## Changes

- **`{Narrator}` placeholder** (`core/naming.py`,
`download/postprocess/transfer.py`, `core/models.py`): `DownloadTask`
gains `narrator`, read from `narrator` or `extra.narrator` when a
release is queued, and kept in the restart-safe retry payload. It's
available in audiobook templates like any other placeholder, including
prefix/suffix blocks such as `{ - Narrator}`.
- **Audiobookshelf folder style**: Audiobookshelf reads the narrator
from folder names like `Title {Narrator}`. The template `{Title}
{{Narrator}}` already matches as `{` + `{Narrator}`; the parser now also
drops the closing brace when the narrator is empty, so the folder is
`Title` rather than `Title }`.
- **No stray spaces around `/`**: an empty placeholder at the start or
end of a folder name no longer leaves a space there (e.g. `Title
/Title`). This applies to all templates.
- **Series fallback** (`release_sources/prowlarr/mam.py`, `source.py`,
`download/orchestrator.py`): MAM enrichment also stores the first
series' name and number as `extra.series_name` /
`extra.series_position`, which queueing already falls back to when the
metadata provider has no series. The provider's series still wins. A
release-level series number is only used when it names the same series
as the provider, so a provider series is never paired with the number of
a different MAM series (an omnibus, for example).
- **Settings and preview** (`config/settings.py`,
`namingTemplatePreview.ts`): the audiobook template descriptions list
`{Narrator}` and explain `{{Narrator}}`. The settings page preview
mirrors the parser changes and offers `{Narrator}` under "Insert
variable" for audiobook templates only.
- **Docs**: regenerated `environment-variables.md`. This also picked up
the Library Check settings, which weren't in the generated docs yet;
happy to drop that part to keep the diff focused.

## Example

Audiobook Path Template `{Author}/{Title} {{Narrator}}/{Title}`:

| Narrator | Result |
|----------|--------|
| Samuel Roukin | `Christopher Ruocchio/Empire of Silence {Samuel
Roukin}/Empire of Silence.m4b` |
| none | `Christopher Ruocchio/Empire of Silence/Empire of Silence.m4b`
|

## Testing

- `tests/core/test_narrator_template_variable.py` (new, 16 tests):
`{{Narrator}}` with and without a value, the `{ {Narrator}}` prefix
form, prefix/suffix blocks, sanitizing, `build_library_path`, task
metadata, retry payload round trip, MAM series name/number parsing
(including a `1-3` range having no number), and queueing (narrator from
`extra`, series fallback, no cross-series number, blank narrator).
- `namingTemplatePreview.test.ts`: the same `{{Narrator}}` cases for the
preview.
- Updated `test_generate_env_docs.py` for the new template description.
- Full `pytest -m "not integration and not e2e"` compared with an
upstream `main` worktree on the same machine: no new failures (the
remaining ones are Windows-only on both).
- `ruff check`, `ruff format --check`, `basedpyright` (0 errors) and
`vulture` on touched files; frontend `tsc --noEmit`, `oxlint`, `oxfmt
--check`, `vitest` (218 passed).
- Checked the settings page locally: the audiobook preview renders `...
{Simon Vance}.mp3` and `{Narrator}` is listed for audiobook templates
only.

No behavior change for existing templates, apart from spaces next to `/`
being trimmed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants