Skip to content

Library optimizations#403

Merged
therobbiedavis merged 2 commits intocanaryfrom
feature/library-optimizations
Mar 9, 2026
Merged

Library optimizations#403
therobbiedavis merged 2 commits intocanaryfrom
feature/library-optimizations

Conversation

@therobbiedavis
Copy link
Collaborator

Small optimization release. I noticed that for users with larger libraries, the library page would take a long time to load. This was due to some redundant and unnecessary checks. Also the the api endpoint was returning a lot of data that was not needed for the page. Additionally I have added url resolution normalizations to download client host/ip:port connection strings so the connection tests are more robust.

Added

  • Shared backend/frontend audiobook status helpers so list views can use a consistent status model without relying on full file metadata from /library
  • A dedicated slim library list DTO for GET /library
  • A shared download-client URI builder for normalizing host, scheme, port, and path handling
  • Backend regression tests covering:
    • slim /library payload behavior
    • wanted-flag correctness
    • NZBGet host normalization and queue-path handling
    • qBittorrent, SABnzbd, and Transmission host normalization

Changed

  • Slimmed GET /library from a hybrid list/detail payload into a lighter list response
  • Kept GET /library/{id} as the rich full-detail audiobook endpoint
  • Moved library list status evaluation to shared backend/frontend helpers instead of deriving it ad hoc in views
  • Switched the app from timer-based Wanted badge refreshes to store-driven updates backed by existing SignalR events
  • Updated the frontend library store to collapse concurrent /library requests into a single in-flight fetch
  • Reused cached library state for app-shell lookups and related UI flows instead of issuing redundant library requests
  • Standardized host/URL interpretation across NZBGet, qBittorrent, SABnzbd, and Transmission for both test/save and runtime monitor paths

Fixed

  • Slow /library responses caused by per-audiobook filesystem existence checks during list loading
  • Duplicate /library requests during startup and view initialization
  • Full-library polling churn for the Wanted badge
  • Extra DB work in audiobook detail loading by collapsing a two-query existence/fetch path into a single no-tracking query
  • NZBGet malformed host parsing that could produce errors like Name or service not known (http:80)
  • The same malformed host/URL bug class across other download clients when users pasted scheme/path data into host fields

Removed

  • Periodic full-library polling for the Wanted badge
  • Per-item filesystem probes from the main /library list path
  • Redundant client-side dependence on full files[] metadata in library list views for status calculation

Continue #401

Recognize additional lossless codecs (alac, aiff, ape, dsd, wv, wav) in both frontend and backend quality detection and add tests for WavPack. Refactor library wanted-flag computation to prefer DB file rows while honoring the legacy FilePath during upgrade: introduce ComputeWantedFlag overload, avoid touching the filesystem on list endpoints, and expose the transitional FilePath on the DTO with a comment. Consolidate active download statuses into a shared static array and adjust queries to use it. Update AudiobookStatusEvaluator ComputeStatus signature/logic to use hasAnyFile (legacy summary + tracked files) and to treat absence of tracked files but presence of a legacy summary as a quality-match; update tests accordingly.
Copilot AI review requested due to automatic review settings March 9, 2026 01:22
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR optimizes library list loading by moving status/wanted evaluation to shared helpers and returning a slimmer /library payload, while improving robustness of quality/status computation (including legacy file summaries and additional lossless formats) and adding regression coverage.

Changes:

  • Updated backend library list endpoint to compute wanted/status without per-item filesystem checks and to support legacy FilePath summaries.
  • Updated shared audiobook status evaluation (backend + frontend) to handle legacy summaries and treat additional formats (e.g., WavPack) as lossless.
  • Added/updated backend and frontend tests for wanted/status behavior and lossless detection.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.

Show a summary per file
File Description
listenarr.api/Controllers/LibraryController.cs Slim /library list composition; set-based lookups for files/profiles/downloads; wanted/status computation updates.
listenarr.api/Services/AudiobookStatusEvaluator.cs Status evaluation signature change (hasAnyFile), legacy-summary handling, expanded lossless detection.
listenarr.api/Models/LibraryAudiobookListItemDto.cs Retains legacy file summary fields in slim list DTO (with clarifying comment).
tests/Listenarr.Api.Tests/LibraryController_WantedFlagRegressionTests.cs Adds regression test ensuring legacy FilePath doesn’t flip items to Wanted.
tests/Listenarr.Api.Tests/AudiobookStatusEvaluatorTests.cs Updates tests for new evaluator signature; adds legacy-summary and WavPack lossless tests.
fe/src/utils/audiobookStatus.ts Expands lossless detection to include additional containers/codecs (incl. wv).
fe/src/tests/audiobookStatus.spec.ts Adds WavPack lossless status unit test.
Comments suppressed due to low confidence (2)

listenarr.api/Controllers/LibraryController.cs:637

  • audiobookIds.Contains(f.AudiobookId) will translate to a large SQL IN (...) list. With SQLite this can hit the provider parameter limit (commonly 999) for users with large libraries, causing /library to fail—the exact scenario this PR targets. Consider rewriting this filter as a join/subquery (so the DB can evaluate it without expanding thousands of parameters), or chunk audiobookIds into batches and union the results in memory.
            var audiobookIds = audiobooks.Select(a => a.Id).ToArray();
            var fileSummaries = await _dbContext.AudiobookFiles
                .AsNoTracking()
                .Where(f => audiobookIds.Contains(f.AudiobookId))
                .Select(f => new AudiobookFileStatusInfo

tests/Listenarr.Api.Tests/AudiobookStatusEvaluatorTests.cs:21

  • The test name ComputeStatus_ReturnsNoFile_WhenWanted no longer matches the behavior under test (the wanted parameter was removed; this is now asserting NoFile when hasAnyFile is false). Renaming the test to reflect the new inputs/meaning would keep the suite readable and prevent confusion during future refactors.
        [Fact]
        public void ComputeStatus_ReturnsNoFile_WhenWanted()
        {
            var status = AudiobookStatusEvaluator.ComputeStatus(
                isDownloading: false,
                hasAnyFile: false,
                audiobookQuality: null,
                qualityProfile: null,
                files: null);

            Assert.Equal(AudiobookStatusEvaluator.NoFile, status);
        }

You can also share your feedback on Copilot code review. Take the survey.

@therobbiedavis
Copy link
Collaborator Author

therobbiedavis commented Mar 9, 2026

@tsolo4ever Sorry I merged before I saw your comment. Thanks for that, it was a useful pass. The only thing I didn't address from your comment was about the FilePath. It's still on the slim DTO for now. That is intentional as a transitional field because the current frontend/filtering behavior still uses it. I added a comment to make that explicit. I agree it should eventually go away once the remaining callers fully move to fileCount/status.

Stop expanding an in-memory audiobook ID array into the DB query — fetch AudiobookFiles summaries directly since the endpoint already loads the audiobook table, avoiding large IN() expansion and potential performance issues. Also rename the unit test ComputeStatus_ReturnsNoFile_WhenWanted to ComputeStatus_ReturnsNoFile_WhenHasNoFiles to better reflect the tested condition.
@therobbiedavis therobbiedavis merged commit eef8529 into canary Mar 9, 2026
10 checks passed
@therobbiedavis therobbiedavis deleted the feature/library-optimizations branch March 9, 2026 01:39
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.

2 participants