Skip to content

Fix SWR metadata refresh, rounded images, and Jellyfin 12 compatibility - #133

Merged
crowquillx merged 7 commits into
mainfrom
agent/reliability-jellyfin12
Aug 11, 2026
Merged

Fix SWR metadata refresh, rounded images, and Jellyfin 12 compatibility#133
crowquillx merged 7 commits into
mainfrom
agent/reliability-jellyfin12

Conversation

@crowquillx

@crowquillx crowquillx commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Summary

  • update metadata-only stale-while-revalidate results in place with role-specific dataChanged notifications, preserving delegate identity, scroll, and focus
  • replace the ineffective rectangular software rounded-image fallback with a real CPU-rounded rendering path and visual pixel assertions
  • make QML-facing rounded-image cache lookup non-blocking while retaining cache-worker ownership, request coalescing, and generation invalidation
  • move Qt Test behind BUILD_TESTING and remove NO_CACHEGEN after verifying cache generation in a production build
  • audit Bloom's Jellyfin calls against the pinned Jellyfin 12 OpenAPI contract and migrate legacy/obsolete routes and parameters

Root causes and impact

Library refresh equality only considered total count and item identity/order, so canonical metadata changes could be discarded. RoundedImage relied on Rectangle.clip, whose clip remains rectangular in Qt Quick's software backend. Rounded-image delegates also synchronously crossed into the image-cache worker for lookup/touch operations, allowing a busy SQLite worker to stall the UI thread.

The changes preserve structural model resets for insertions, removals, and reordering while applying stable-row metadata updates in place. Cached rounded variants are now discovered and touched asynchronously, duplicate requests remain coalesced, and stale generation results are rejected after cache invalidation.

The Jellyfin audit pins an exact Jellyfin 12 OpenAPI revision and updates library, item detail, playback, segment, session, and authentication fallback behavior to supported routes and request shapes.

Validation

  • ./scripts/dev-build.sh --tests --jobs 4
  • QT_QPA_PLATFORM=offscreen ctest --test-dir build-dev --output-on-failure -j4 — 36/36 passed
  • provider contract tests — 31/31 passed
  • contract validator — 42 checks across 3 deployments
  • clean Release BUILD_TESTING=OFF build — 272/272 targets, including QML cache generation
  • nix flake check --print-build-logs
  • nix build --print-build-logs
  • ./scripts/run-clang-tidy.sh
  • git diff --check
  • Jellyfin 12.0.0 RC3 unauthenticated route smoke: migrated routes reached authorization handling rather than returning 404

An authenticated Jellyfin 12 server with representative media was not available, so end-to-end authenticated library/playback behavior remains a live-provider validation item.

Documentation

Updated build, image-cache, canonical model/viewmodel, playback, media-segment, and provider-compatibility documentation.

Note

Fix SWR metadata refresh, rounded images, and update Jellyfin API routes to v12

  • SWR metadata refresh: LibraryViewModel now compares full canonical item objects instead of only IDs, emitting fine-grained dataChanged signals per row for metadata-only updates without a full model reset.
  • Rounded images: RoundedImage gains a CPU-painted software fallback (SoftwareRoundedImage) that snapshots the already-loaded base image via QPainter with a rounded clip path when shaders are unavailable, avoiding duplicate network loads. The ImageCacheProvider rounded-image path is reworked with revision-aware lookups, async LRU touches, and retry semantics.
  • Jellyfin 12 API compatibility: Catalog, playback, and session endpoints are migrated to Jellyfin 12 OpenAPI routes — /Items replaces user-scoped paths, POST /Items/{itemId}/PlaybackInfo sends UserId in the JSON body, DELETE /Devices replaces POST /Sessions/{id}/Logout for revocation, ApiKey replaces api_key in URLs, and EventName is dropped from progress reports.
  • Media segments: Intro Skipper plugin probes are removed; media segments are fetched from the core /MediaSegments/{id} endpoint via JellyfinModelMapper::mediaSegments.
  • Contract validation: A new OpenAPI manifest generator and pinned Jellyfin 12.0 manifest enforce API surface compliance in CI, with tests verifying required/forbidden routes and schema invariants.
  • Risk: All Jellyfin API call shapes change; any server older than v12 will likely receive malformed or unrecognized requests.

Macroscope summarized 109e34d.

Summary by CodeRabbit

  • New Features
    • Improved Jellyfin compatibility across catalog, playback, artwork, media-segment, and session APIs.
    • Added support for standard Jellyfin media segments and more reliable playback metadata.
    • Added software-rendering support for rounded artwork when shader acceleration is unavailable.
  • Bug Fixes
    • Library refreshes now update changed metadata and artwork without resetting the entire view.
    • Improved asynchronous image caching, reuse, and responsiveness.
    • Updated session revocation and authentication handling for greater reliability.
  • Documentation
    • Updated provider compatibility, playback, build, image, and model documentation.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR pins Jellyfin compatibility to the official 12.0 OpenAPI contract, updates catalog, playback, media-segment, and session routes, adds asynchronous rounded-image handling with a software fallback, and changes stable library refreshes to update rows in place.

Changes

Jellyfin compatibility and playback

Layer / File(s) Summary
Contract baseline and validation
AGENTS.md, docs/provider-compatibility.md, tests/contracts/*
Jellyfin routes, parameters, smoke tests, OpenAPI metadata, and excluded operations now use the pinned 12.0 contract.
Catalog and playback routes
src/providers/jellyfin/JellyfinCatalogProvider.h, src/network/PlaybackService.cpp, src/providers/jellyfin/JellyfinPlaybackProvider.*, tests/ProviderCatalogTest.cpp, tests/CanonicalModelsTest.cpp
Requests use encoded identifiers, reduced fields, ApiKey, array response handling, PlaybackInfoDto.UserId, and updated report payloads.
Media segments and sessions
src/providers/jellyfin/JellyfinModelMapper.*, src/network/SessionService.*, src/network/PlaybackService.cpp, tests/contracts/run_live_contracts.py
Core /MediaSegments/{id} responses are mapped from ticks to milliseconds. Session revocation deletes deduplicated devices and removes matching local sessions.

Rounded-image rendering

Layer / File(s) Summary
Asynchronous cache pipeline
src/ui/ImageCacheProvider.*, src/ui/ImageCacheStore.*, tests/ArtworkRefreshTest.cpp
Rounded-image lookup and generation run asynchronously with coalescing, cache revisions, invalidation, ready notifications, and worker-control test hooks.
Shader and software rendering
src/ui/RoundedImage.qml, src/ui/SoftwareRoundedImage.*, src/ui/LibraryScreen.qml, tests/RoundedImageTest.cpp
The UI shares one base image between shader and CPU-painted paths. Software rendering captures and paints a rounded snapshot asynchronously.

Library refresh and build configuration

Layer / File(s) Summary
Stable-row refreshes
src/viewmodels/LibraryViewModel.cpp, tests/LibraryViewModelCanonicalTest.cpp, docs/viewmodels.md
Complete canonical objects are compared. Stable structures update changed rows with targeted dataChanged roles. Structural changes still reset the model.
Build and validation setup
CMakeLists.txt, src/CMakeLists.txt, src/cmake/BloomLibraries.cmake, tests/CMakeLists.txt, docs/build.md, docs/canonical-models.md
Qt Test is required only for testing builds. QML cache generation remains enabled. New software-image sources and testing definitions are wired into the build.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • crowquillx/Bloom#82 — Extends the provider-compatibility baseline with Jellyfin 12 contract metadata and route validation.
  • crowquillx/Bloom#126 — Shares the ImageCacheProvider and RoundedImage.qml implementation areas.
  • crowquillx/Bloom#120 — Shares LibraryViewModel SWR and canonical metadata refresh behavior.

Suggested labels: codex

Poem

A rabbit checks each route in line,
While rounded pixels load just fine.
The cache hops off the worker’s way,
Stable rows refresh without delay.
Jellyfin’s contract stands quite bright,
And software corners bloom in light.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely identifies the pull request's main changes: SWR metadata refresh, rounded images, and Jellyfin 12 compatibility.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/reliability-jellyfin12

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@crowquillx
crowquillx marked this pull request as ready for review August 11, 2026 16:29
Comment thread src/ui/ImageCacheProvider.cpp
Comment thread src/providers/jellyfin/JellyfinCatalogProvider.h
Comment thread src/network/SessionService.cpp
Comment thread src/ui/ImageCacheProvider.h
Comment thread src/cmake/BloomLibraries.cmake Outdated
Comment thread src/ui/SoftwareRoundedImage.cpp
@greptile-apps

greptile-apps Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR updates SWR metadata refreshes, introduces asynchronous and software-rendered rounded-image paths, gates test-only Qt dependencies, and migrates Jellyfin requests to the pinned Jellyfin 12 contract.

  • Stable library rows now receive role-specific data updates instead of model resets.
  • Rounded variants use asynchronous cache discovery and a CPU-painted software fallback.
  • Jellyfin catalog, playback, segment, state, and remote-revocation requests use revised routes and payloads.
  • Bulk device revocation currently reports completion and clears loading before all deletions settle.

Confidence Score: 4/5

The bulk Jellyfin revocation lifecycle should be fixed before merging because it reports completion and re-enables controls while device deletions remain in flight.

Multiple device deletions share a single boolean loading state, each completion clears that state independently, and the aggregate completion signal is emitted immediately after requests are scheduled.

Files Needing Attention: src/network/SessionService.cpp and tests/CMakeLists.txt

Important Files Changed

Filename Overview
src/network/SessionService.cpp Migrates Jellyfin revocation to device deletion, but bulk operations do not aggregate asynchronous completion or loading state.
src/ui/ImageCacheProvider.cpp Adds non-blocking rounded-variant lookup, coalescing, ready-map touches, and generation-aware invalidation.
src/ui/RoundedImage.qml Replaces rectangular software clipping with a lazily loaded CPU-painted fallback.
src/ui/SoftwareRoundedImage.cpp Implements generation-checked item capture and antialiased rounded CPU painting.
src/viewmodels/LibraryViewModel.cpp Detects full canonical metadata changes and updates stable rows with targeted dataChanged notifications.
src/providers/jellyfin/JellyfinCatalogProvider.h Migrates catalog and state operations to Jellyfin 12 routes and response envelopes.
src/network/PlaybackService.cpp Updates playback information, segment, item lookup, trickplay, and played-state requests for Jellyfin 12.
tests/CMakeLists.txt Adds rounded-image implementation sources directly to two tests, contrary to the production-target linking policy.

Sequence Diagram

sequenceDiagram
    participant UI as Active Sessions UI
    participant SS as SessionService
    participant JF as Jellyfin
    UI->>SS: revokeAllOtherSessions()
    SS->>JF: GET /Sessions
    JF-->>SS: sessions grouped by device
    loop Each distinct remote device
        SS->>JF: "DELETE /Devices?id=deviceId"
    end
    SS-->>UI: allOtherSessionsRevoked(count)
    Note over SS,UI: Emitted before DELETE requests settle
    JF-->>SS: First DELETE completes
    SS-->>UI: "isLoading=false"
    JF-->>SS: Remaining DELETE completions
Loading

Comments Outside Diff (1)

  1. src/network/SessionService.cpp, line 288-292 (link)

    P1 Bulk revocation completes prematurely

    If at least two distinct remote Jellyfin devices are active, the loop starts multiple asynchronous deletions but emits allOtherSessionsRevoked immediately, and the first completed request clears isLoading while the others remain in flight, causing session controls to be re-enabled before bulk revocation finishes.

    Knowledge Base Used: Network Services

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/network/SessionService.cpp
    Line: 288-292
    
    Comment:
    **Bulk revocation completes prematurely**
    
    If at least two distinct remote Jellyfin devices are active, the loop starts multiple asynchronous deletions but emits `allOtherSessionsRevoked` immediately, and the first completed request clears `isLoading` while the others remain in flight, causing session controls to be re-enabled before bulk revocation finishes.
    
    **Knowledge Base Used:** [Network Services](https://app.greptile.com/bloom-org-2/-/custom-context/knowledge-base/crowquillx/bloom/-/docs/network-services.md)
    
    ---
    
    For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
Prompt To Fix All With AI
### Issue 1
src/network/SessionService.cpp:288-292
**Bulk revocation completes prematurely**

If at least two distinct remote Jellyfin devices are active, the loop starts multiple asynchronous deletions but emits `allOtherSessionsRevoked` immediately, and the first completed request clears `isLoading` while the others remain in flight, causing session controls to be re-enabled before bulk revocation finishes.

### Issue 2
tests/CMakeLists.txt:487-490
**Tests rebuild production implementation**

`RoundedImageTest` and `VisualRegressionTest` compile `SoftwareRoundedImage.cpp` directly instead of linking the production target that owns it, allowing tests to exercise copies built with dependencies or definitions that differ from the application.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "fix: harden reliability and Jellyfin 12 ..." | Re-trigger Greptile

Comment thread tests/CMakeLists.txt Outdated
@coderabbitai coderabbitai Bot added the codex label Aug 11, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/network/SessionService.cpp (1)

267-292: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Wait for device deletions before emitting bulk success.

Lines 281-288 start asynchronous requests. Line 292 emits allOtherSessionsRevoked before any DELETE completes. A later failure can leave a remote device authenticated after the UI reports success. Track pending device deletions and emit the success count only after all requests succeed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/network/SessionService.cpp` around lines 267 - 292, The sessionsLoaded
handler currently emits allOtherSessionsRevoked immediately after starting
asynchronous revokeSession calls. Track pending device deletions and their
failures around revokeSession, then emit allOtherSessionsRevoked only after
every requested deletion completes successfully; propagate an operationFailed
result if any deletion fails, while preserving the current-device exclusion and
revoked count.
🧹 Nitpick comments (7)
src/ui/RoundedImage.qml (1)

69-97: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Geometry refreshes are scheduled twice.

SoftwareRoundedImage already connects widthChanged and heightChanged to requestRefresh(). The loaded item fills the Loader, so a size change of root also changes the item size. Lines 89-90 therefore schedule a second requestRefresh() for the same geometry change, which discards the first capture and starts another grabToImage().

Consider limiting onWidthChanged/onHeightChanged to refreshStaticShaderSource() and letting the C++ item handle its own size changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ui/RoundedImage.qml` around lines 69 - 97, Update the onWidthChanged and
onHeightChanged handlers in RoundedImage so they only call
refreshStaticShaderSource(), removing the refreshSoftwareFallback path for root
geometry changes. Keep other refreshRenderPaths triggers unchanged and rely on
SoftwareRoundedImage’s own size-change handling for fallback image refreshes.
src/ui/LibraryScreen.qml (1)

1712-1721: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Qualify the root-scope property reads.

Lines 1713 read roundedImageMode and roundedPreprocessEnabled without a qualifier. Both are declared on root at Line 508-509. The Connections block at Line 1730-1738 already targets root for the same properties. Use root.roundedImageMode and root.roundedPreprocessEnabled so the lookup is explicit and qmllint stays clean.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ui/LibraryScreen.qml` around lines 1712 - 1721, Update
getRoundedImageSource to qualify both root-declared property reads as
root.roundedImageMode and root.roundedPreprocessEnabled, matching the existing
root-targeted Connections usage and preserving the current condition behavior.
tests/RoundedImageTest.cpp (2)

83-91: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert that both source replacements matched.

Lines 84-90 patch the production QML by exact byte-string match. The second pattern depends on exactly 12 leading spaces before SoftwareRoundedImage { in src/ui/RoundedImage.qml. A reindentation of that file makes the replacement a no-op. The component then fails to resolve the type, and the test reports a generic QML error instead of the real cause.

Verify the replacement count.

♻️ Proposed fix that fails with a clear message
     QByteArray qmlSource = qmlFile.readAll();
+    QVERIFY2(qmlSource.contains(QByteArrayLiteral("SoftwareRoundedImage {")),
+             "RoundedImage.qml no longer instantiates SoftwareRoundedImage");
+    const qsizetype typeOffset =
+        qmlSource.indexOf(QByteArrayLiteral("SoftwareRoundedImage {"));
     qmlSource.replace(
         QByteArrayLiteral("import QtQuick\n"),
         QByteArrayLiteral(
             "import QtQuick\nimport BloomInternal 1.0 as BloomInternal\n"));
-    qmlSource.replace(
-        QByteArrayLiteral("            SoftwareRoundedImage {"),
-        QByteArrayLiteral("            BloomInternal.SoftwareRoundedImage {"));
+    Q_UNUSED(typeOffset);
+    const QByteArray before = qmlSource;
+    qmlSource.replace(QByteArrayLiteral("SoftwareRoundedImage {"),
+                      QByteArrayLiteral("BloomInternal.SoftwareRoundedImage {"));
+    QVERIFY2(qmlSource != before, "SoftwareRoundedImage type name was not qualified");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/RoundedImageTest.cpp` around lines 83 - 91, Update the replacement
logic in the RoundedImageTest setup to verify that each exact byte-string
replacement matched once. Assert the replacement count for both import and
SoftwareRoundedImage substitutions before calling component.setData, so
formatting changes fail with a clear test message.

163-177: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for a refresh after the snapshot is ready.

The software branch validates the first snapshot only. It does not exercise requestRefresh() after softwareFallbackReady becomes true. That path is the one where baseImage.visible is already false when the next grabToImage() runs, which is the risk raised on src/ui/RoundedImage.qml Line 120-122.

Resize the item after the first assertion and re-assert the corner and center pixels.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/RoundedImageTest.cpp` around lines 163 - 177, Extend the software
branch after the existing corner and center assertions to resize the item,
trigger the refresh path via the established resize behavior, wait for the
updated snapshot, and capture the window again. Re-assert the corner matches
background and the center matches blue, covering requestRefresh() after
softwareFallbackReady is true.
src/ui/ImageCacheProvider.cpp (1)

1005-1054: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace the repeated --m_activeRoundedTasks with a scope guard.

The worker lambda decrements m_activeRoundedTasks at five separate return points. Any future early return that misses the decrement leaks the counter and corrupts the activeRoundedTasks statistic permanently. A single guard at the top of the lambda removes that class of defect.

♻️ Proposed refactor
     ++m_activeRoundedTasks;
     auto future = QtConcurrent::run(
         &m_threadPool,
         [this, url, key, sourcePath, radiusPx, targetSize, generation]() {
+        const QScopeGuard taskGuard([this]() { --m_activeRoundedTasks; });
         RoundedTaskResult result;
         if (m_cacheGeneration.load() != generation || !m_store) {
-            --m_activeRoundedTasks;
             return result;
         }

Remove the remaining --m_activeRoundedTasks; statements and add #include <QScopeGuard>.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ui/ImageCacheProvider.cpp` around lines 1005 - 1054, Update the worker
lambda passed to QtConcurrent::run so it creates a QScopeGuard at the start that
decrements m_activeRoundedTasks on scope exit, then remove all explicit
--m_activeRoundedTasks statements from its return paths. Add the QScopeGuard
include required by this guard.
src/cmake/BloomLibraries.cmake (1)

117-119: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider a dedicated option instead of BUILD_TESTING for the test hook.

BUILD_TESTING defaults to ON as soon as include(CTest) runs. Any configuration that enables CTest then compiles blockCacheWorkerForTest and blockWorkerForTest into the shipped Bloom binary, because the definition is PUBLIC and the executable links Bloom::ImageCache. The hook blocks the cache worker thread indefinitely until a semaphore release. A separate option, for example BLOOM_ENABLE_TEST_HOOKS, keeps the hook out of release artifacts while BUILD_TESTING remains free to control test registration.

PUBLIC is correct here and must stay, otherwise the header declaration and the library definition diverge between the library and its consumers.

♻️ Proposed gating change
-if(BUILD_TESTING)
+option(BLOOM_ENABLE_TEST_HOOKS "Compile test-only synchronization hooks" ${BUILD_TESTING})
+if(BLOOM_ENABLE_TEST_HOOKS)
     target_compile_definitions(BloomImageCache PUBLIC BLOOM_TESTING=1)
 endif()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cmake/BloomLibraries.cmake` around lines 117 - 119, Replace the
BUILD_TESTING condition around target_compile_definitions with a dedicated
BLOOM_ENABLE_TEST_HOOKS option, defaulting it OFF so test hooks are excluded
from normal and release builds while CTest can still control test registration.
Keep the BLOOM_TESTING definition PUBLIC for Bloom::ImageCache consumers, and
ensure the new option is declared in the project’s existing CMake option
configuration.
tests/ArtworkRefreshTest.cpp (1)

522-532: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The 150 ms wall-clock threshold is flaky on loaded CI runners.

The release happens after 300 ms, so the assertion has only a 150 ms margin. On a contended runner, thread scheduling and the QMetaObject::invokeMethod dispatch alone can consume that margin without any regression in the non-blocking behavior. The test then fails for reasons unrelated to the property under test.

Assert ordering instead of duration: check that requestRoundedImage returns while releaseWorker is still unreleased. That is the actual invariant and it does not depend on machine speed.

♻️ Proposed refactor: assert ordering, keep a generous time bound
-    QElapsedTimer timer;
-    timer.start();
+    std::atomic<bool> released{false};
+    auto delayedRelease = QtConcurrent::run([&releaseWorker, &released]() {
+        QThread::msleep(300);
+        released.store(true);
+        releaseWorker.release();
+    });
+
+    QElapsedTimer timer;
+    timer.start();
     QCOMPARE(cache.requestRoundedImage(
                  QStringLiteral("https://images.example.test/busy-worker.png"),
                  16, 32, 32),
              QString());
+    QVERIFY2(!released.load(),
+             "rounded request did not return before the cache worker resumed");
     const qint64 elapsedMs = timer.elapsed();
-    QVERIFY2(elapsedMs < 150,
+    QVERIFY2(elapsedMs < 300,
              qPrintable(QStringLiteral(
                  "rounded request blocked for %1 ms on cache worker")
                             .arg(elapsedMs)));

Move the existing delayedRelease declaration to the position shown above.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/ArtworkRefreshTest.cpp` around lines 522 - 532, Replace the
elapsed-time assertion around requestRoundedImage with an ordering assertion
that verifies the worker remains unreleased when the request returns. Move
delayedRelease’s declaration before the request as indicated, retain a generous
timeout only if needed to prevent hangs, and remove the strict 150 ms wall-clock
threshold.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/network/SessionService.cpp`:
- Around line 219-244: Update the session-revocation flow around
deviceIdForSession and onRevokeSessionFinished to detect self-revocation by
comparing the target deviceId with the current device identity, not only by
comparing sessionId with m_currentSessionId. When they match, emit
selfSessionRevoked even if the selected session differs from m_currentSessionId,
while preserving the existing behavior for other devices.

In `@src/ui/ImageCacheProvider.cpp`:
- Around line 1089-1095: Update the rounded-render completion handling around
renderRoundedPng and processPendingRounded so terminal render or save failures
remove the corresponding m_pendingRounded entry and emit the existing
failure/fallback signal, preventing callers from waiting indefinitely. Also
bound or remove stale m_knownBaseImages records after processing, while
preserving the retry behavior for valid known-base entries.
- Around line 1194-1221: In ImageCacheProvider::touchRoundedVariantAsync, limit
m_cacheMutationMutex to checking the current generation and m_store, then
release it before calling m_store->touch(key). Preserve the guarded touch
behavior by only invoking touch when both conditions were true, avoiding any
mutex hold during the blocking store call.
- Around line 1153-1191: Update the rounded-variant and known-base-image caching
flow around m_readyRounded, m_knownBaseImages, and lookupEntry() to track
ImageCacheStore::LookupResult::revision per cache key instead of using the
global m_cacheContentRevision for validity. Preserve entries whose individual
revisions remain valid after unrelated writes, and remove or invalidate the
corresponding in-memory entries whenever eviction or deletion removes a cache
key.

In `@src/ui/ImageCacheStore.cpp`:
- Around line 882-896: Update ImageCacheStore::blockWorkerForTest so the queued
lambda uses a bounded QSemaphore wait instead of indefinitely calling
release->acquire(), allowing the worker to resume when the timeout expires.
Document that callers must keep both stack-owned semaphores alive until the
worker resumes, while preserving the existing entered notification and test-only
scope.

In `@src/ui/LibraryScreen.qml`:
- Around line 1789-1794: Update the Image error handling near
delegateItem.getImageSource() so it records the failure in a separate property
instead of assigning directly to source. Preserve the source binding for live
delegates, and use the error state only to represent the failed load without
requiring GridView.onReused to restore the binding.

In `@src/ui/SoftwareRoundedImage.cpp`:
- Around line 61-112: Update SoftwareRoundedImage::requestRefresh() to schedule
the initial retry without replacing m_retryTimer’s configured 32 ms interval,
and add a retry-attempt counter checked by attemptGrab() to stop retries after a
bounded maximum. Reset that counter in requestRefresh(), increment it only for
retryable failures (missing window, failed grab, or null image), and preserve
the existing generation and readiness behavior.
- Around line 87-92: Update the capture target construction in the image-grab
flow to multiply both dimensions by
m_sourceItem->window()->effectiveDevicePixelRatio(), while preserving the
minimum-size and rounding behavior. Also handle ItemDevicePixelRatioHasChanged
in the relevant item-change logic so the capture refreshes when the screen DPR
changes, not only when windowChanged fires.

In `@tests/CMakeLists.txt`:
- Around line 487-497: Introduce or reuse the narrowest production Bloom::* UI
library owning SoftwareRoundedImage, then update tests/CMakeLists.txt lines
487-497 for RoundedImageTest and lines 1017-1018 for VisualRegressionTest to
remove SoftwareRoundedImage.h and SoftwareRoundedImage.cpp from each target’s
sources and link that same library instead, preserving an acyclic production
dependency structure.

In `@tests/contracts/provider-contracts.json`:
- Around line 511-513: Update the contract entry’s method value for the
composite PlaybackInfo and AdditionalParts flow to represent both operations,
using the established POST/GET convention or splitting it into separate
contracts while preserving the existing paths and request semantics.

In `@tests/contracts/validate_contracts.py`:
- Around line 163-171: The Jellyfin OpenAPI pin must be backed by a real
checksum-verified artifact rather than repeated metadata and handwritten
assertions. In tests/contracts/validate_contracts.py lines 163-171, replace the
blob URL-only validation with validation of the stored raw or generated manifest
artifact and derive the OpenAPI assertions from it; update
tests/contracts/provider_contracts_test.py lines 156-201 and
tests/ProviderCatalogTest.cpp lines 290-330 to consume the same derived data
instead of literals or handwritten openApiItemFields. Document artifact
retrieval and checksum verification in docs/provider-compatibility.md lines
33-44.

---

Outside diff comments:
In `@src/network/SessionService.cpp`:
- Around line 267-292: The sessionsLoaded handler currently emits
allOtherSessionsRevoked immediately after starting asynchronous revokeSession
calls. Track pending device deletions and their failures around revokeSession,
then emit allOtherSessionsRevoked only after every requested deletion completes
successfully; propagate an operationFailed result if any deletion fails, while
preserving the current-device exclusion and revoked count.

---

Nitpick comments:
In `@src/cmake/BloomLibraries.cmake`:
- Around line 117-119: Replace the BUILD_TESTING condition around
target_compile_definitions with a dedicated BLOOM_ENABLE_TEST_HOOKS option,
defaulting it OFF so test hooks are excluded from normal and release builds
while CTest can still control test registration. Keep the BLOOM_TESTING
definition PUBLIC for Bloom::ImageCache consumers, and ensure the new option is
declared in the project’s existing CMake option configuration.

In `@src/ui/ImageCacheProvider.cpp`:
- Around line 1005-1054: Update the worker lambda passed to QtConcurrent::run so
it creates a QScopeGuard at the start that decrements m_activeRoundedTasks on
scope exit, then remove all explicit --m_activeRoundedTasks statements from its
return paths. Add the QScopeGuard include required by this guard.

In `@src/ui/LibraryScreen.qml`:
- Around line 1712-1721: Update getRoundedImageSource to qualify both
root-declared property reads as root.roundedImageMode and
root.roundedPreprocessEnabled, matching the existing root-targeted Connections
usage and preserving the current condition behavior.

In `@src/ui/RoundedImage.qml`:
- Around line 69-97: Update the onWidthChanged and onHeightChanged handlers in
RoundedImage so they only call refreshStaticShaderSource(), removing the
refreshSoftwareFallback path for root geometry changes. Keep other
refreshRenderPaths triggers unchanged and rely on SoftwareRoundedImage’s own
size-change handling for fallback image refreshes.

In `@tests/ArtworkRefreshTest.cpp`:
- Around line 522-532: Replace the elapsed-time assertion around
requestRoundedImage with an ordering assertion that verifies the worker remains
unreleased when the request returns. Move delayedRelease’s declaration before
the request as indicated, retain a generous timeout only if needed to prevent
hangs, and remove the strict 150 ms wall-clock threshold.

In `@tests/RoundedImageTest.cpp`:
- Around line 83-91: Update the replacement logic in the RoundedImageTest setup
to verify that each exact byte-string replacement matched once. Assert the
replacement count for both import and SoftwareRoundedImage substitutions before
calling component.setData, so formatting changes fail with a clear test message.
- Around line 163-177: Extend the software branch after the existing corner and
center assertions to resize the item, trigger the refresh path via the
established resize behavior, wait for the updated snapshot, and capture the
window again. Re-assert the corner matches background and the center matches
blue, covering requestRefresh() after softwareFallbackReady is true.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 154ba315-8496-43a0-ab42-5af32b1ece1c

📥 Commits

Reviewing files that changed from the base of the PR and between 7fb4ee3 and dffdd24.

📒 Files selected for processing (43)
  • AGENTS.md
  • CMakeLists.txt
  • docs/build.md
  • docs/canonical-models.md
  • docs/images.md
  • docs/media-segments.md
  • docs/playback.md
  • docs/provider-compatibility.md
  • docs/viewmodels.md
  • src/CMakeLists.txt
  • src/cmake/BloomLibraries.cmake
  • src/network/LibraryService.cpp
  • src/network/PlaybackService.cpp
  • src/network/SessionService.cpp
  • src/network/SessionService.h
  • src/providers/jellyfin/JellyfinCatalogProvider.h
  • src/providers/jellyfin/JellyfinModelMapper.cpp
  • src/providers/jellyfin/JellyfinModelMapper.h
  • src/providers/jellyfin/JellyfinPlaybackProvider.cpp
  • src/providers/jellyfin/JellyfinPlaybackProvider.h
  • src/providers/jellyfin/JellyfinProviderAdapter.h
  • src/providers/jellyfin/JellyfinRequestFactory.cpp
  • src/ui/ImageCacheProvider.cpp
  • src/ui/ImageCacheProvider.h
  • src/ui/ImageCacheStore.cpp
  • src/ui/ImageCacheStore.h
  • src/ui/LibraryScreen.qml
  • src/ui/RoundedImage.qml
  • src/ui/SoftwareRoundedImage.cpp
  • src/ui/SoftwareRoundedImage.h
  • src/viewmodels/LibraryViewModel.cpp
  • tests/ArtworkRefreshTest.cpp
  • tests/CMakeLists.txt
  • tests/CanonicalModelsTest.cpp
  • tests/LibraryViewModelCanonicalTest.cpp
  • tests/PlayerControllerAutoplayContextTest.cpp
  • tests/ProviderCatalogTest.cpp
  • tests/ProviderTransportTest.cpp
  • tests/RoundedImageTest.cpp
  • tests/contracts/provider-contracts.json
  • tests/contracts/provider_contracts_test.py
  • tests/contracts/run_live_contracts.py
  • tests/contracts/validate_contracts.py
💤 Files with no reviewable changes (1)
  • src/network/LibraryService.cpp

Comment thread src/network/SessionService.cpp
Comment thread src/ui/ImageCacheProvider.cpp
Comment thread src/ui/ImageCacheProvider.cpp
Comment thread src/ui/ImageCacheProvider.cpp
Comment thread src/ui/ImageCacheStore.cpp
Comment thread src/ui/SoftwareRoundedImage.cpp
Comment thread src/ui/SoftwareRoundedImage.cpp
Comment thread tests/CMakeLists.txt
Comment thread tests/contracts/provider-contracts.json Outdated
Comment thread tests/contracts/validate_contracts.py
Comment thread src/network/SessionService.cpp Outdated

Copy link
Copy Markdown
Owner Author

Automated-review follow-up: bulk device revocation now waits for every distinct DELETE callback, keeps the operation active until the last completion, reports the successful count, recognizes the current session by stable device ID, and uses an explicit fetch-scoped pending flag that is cleared on every refresh failure path (8d15509, ead8bb3). The targeted authentication tests pass.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/network/SessionService.cpp (1)

420-451: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Classify 3xx responses as failures

HttpTransport routes only status codes >= 400 to failureHandler. A 3xx response with QNetworkReply::NoError reaches responseHandler, so onRevokeSessionFinished can report a failed DELETE /Devices as successful. HttpRequestOptions::retrySafety already defaults to RetrySafety::Never.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/network/SessionService.cpp` around lines 420 - 451, Update the
HttpTransport response classification so all HTTP 3xx status codes are routed to
failureHandler, not responseHandler, even when QNetworkReply reports NoError.
Preserve the existing retrySafety::Never default and ensure
onRevokeSessionFinished only handles successful DELETE responses.

Source: Coding guidelines

♻️ Duplicate comments (1)
src/ui/SoftwareRoundedImage.cpp (1)

67-75: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve the configured retry interval.

Line 75 changes m_retryTimer to a zero-millisecond interval. Later scheduleRetry() calls then run all retries immediately. If the source is not renderable yet, the component can exhaust MaximumGrabRetries and remain unready.

Use QTimer::singleShot(0, this, &SoftwareRoundedImage::attemptGrab) for the initial attempt.

Proposed fix
-    m_retryTimer.start(0);
+    QTimer::singleShot(0, this, &SoftwareRoundedImage::attemptGrab);
Qt 6 QTimer documentation: Does QTimer::start(int msec) update the interval used by a later QTimer::start() call?
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ui/SoftwareRoundedImage.cpp` around lines 67 - 75, In
SoftwareRoundedImage::requestRefresh(), avoid changing m_retryTimer’s configured
retry interval by replacing the zero-duration timer start with a one-shot queued
invocation of attemptGrab(). Keep the existing timer stop, state reset,
update(), and scheduleRetry() behavior unchanged.
🧹 Nitpick comments (3)
src/network/SessionService.cpp (1)

409-412: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Run the deferred bulk revocation after the load signals.

Line 411 calls revokeLoadedOtherDevices before emit sessionsChanged() and emit sessionsLoaded() at Lines 414-415. revokeLoadedOtherDevices can emit allOtherSessionsRevoked(0), and a synchronously completed delete can emit sessionRevoked and sessionsChanged from inside this block. Consumers then observe revocation results before the refreshed session list is published, and sessionsLoaded arrives last.

Move the trigger below the two emits, or dispatch it with a queued invocation so the refresh completes first.

♻️ Proposed fix
-    if (m_bulkRevokeRefreshPending) {
-        m_bulkRevokeRefreshPending = false;
-        revokeLoadedOtherDevices();
-    }
-
     emit sessionsChanged();
     emit sessionsLoaded();
+
+    if (m_bulkRevokeRefreshPending) {
+        m_bulkRevokeRefreshPending = false;
+        revokeLoadedOtherDevices();
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/network/SessionService.cpp` around lines 409 - 412, In the
session-loading completion flow, move the m_bulkRevokeRefreshPending check and
revokeLoadedOtherDevices() call to after emit sessionsChanged() and emit
sessionsLoaded(). Preserve clearing the pending flag before triggering the
revocation so all refreshed-session signals are delivered first.
tests/contracts/generate_jellyfin_openapi_manifest.py (2)

55-65: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Deduplicate parameters by (name, in) and let operation-level entries replace path-level entries. The current generator emits both entries. The validator keeps the later operation-level entry for identical parameters, but the manifest still contains duplicates and its name-only map conflates parameters with the same name in different locations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/contracts/generate_jellyfin_openapi_manifest.py` around lines 55 - 65,
Update the parameter collection in the manifest generator to deduplicate entries
by the `(name, in)` pair, processing path-level parameters before
operation-level parameters so later operation entries replace matching path
entries. Ensure the final manifest contains only one entry per pair and does not
use name alone as the identity.

120-159: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a Nix regeneration check for the Jellyfin manifest. nix/tests.nix validates only the checked-in manifest and never runs generate_jellyfin_openapi_manifest.py. Fetch the pinned OpenAPI artifact with its fixed hash, generate to a temporary path, and compare it byte-for-byte with jellyfin-12-openapi-manifest.json.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/contracts/generate_jellyfin_openapi_manifest.py` around lines 120 -
159, Add a Nix test in nix/tests.nix that fetches the pinned Jellyfin OpenAPI
artifact with its fixed hash, runs generate_jellyfin_openapi_manifest.py using
the pinned contracts, writes the result to a temporary path, and compares it
byte-for-byte with jellyfin-12-openapi-manifest.json. Keep the existing manifest
validation and ensure the check exercises main’s SHA/version validation.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/network/SessionService.cpp`:
- Around line 285-318: Update revokeLoadedOtherDevices so every queued
revocation that revokeSession cannot dispatch is completed through
finishDeviceRevocation(deviceId, false), or abort the batch while clearing
m_pendingBulkRevokeDeviceIds. Resolve and retain each target device ID before
calling revokeSession, and ensure early authentication, empty-device, or
null-transport returns cannot leave m_isLoading set or prevent
allOtherSessionsRevoked from firing.
- Around line 275-283: Update revokeAllOtherSessions so the m_isLoading
early-return branch emits operationFailed before returning, ensuring callers
receive a terminal signal when the destructive action cannot start; leave the
existing pending-flag and fetchActiveSessions flow unchanged.

In `@src/ui/ImageCacheProvider.h`:
- Around line 183-186: Update the rounded-lookup test hook declarations and
implementation of blockNextRoundedLookupForTest in src/ui/ImageCacheProvider.h
(lines 183-186 and 327-330) to retain both semaphores via
QSharedPointer<QSemaphore> across the worker boundary. In
tests/ArtworkRefreshTest.cpp (lines 560-574), create lookupEntered and
releaseLookup with QSharedPointer<QSemaphore>::create() and pass the shared
pointers to the hook, preserving semaphore usage and timeout behavior.

In `@tests/contracts/validate_contracts.py`:
- Around line 174-181: Update the stream operation handling near the existing
operations validation to retrieve ("GET", "/Videos/{itemId}/stream") safely and
pass its presence through _require, raising ContractValidationError instead of
allowing KeyError. Build stream_parameters only from dictionary parameter
entries, matching the filtering behavior already used around the earlier
parameter-validation loop, then preserve the existing deviceProfileId
deprecation check.

---

Outside diff comments:
In `@src/network/SessionService.cpp`:
- Around line 420-451: Update the HttpTransport response classification so all
HTTP 3xx status codes are routed to failureHandler, not responseHandler, even
when QNetworkReply reports NoError. Preserve the existing retrySafety::Never
default and ensure onRevokeSessionFinished only handles successful DELETE
responses.

---

Duplicate comments:
In `@src/ui/SoftwareRoundedImage.cpp`:
- Around line 67-75: In SoftwareRoundedImage::requestRefresh(), avoid changing
m_retryTimer’s configured retry interval by replacing the zero-duration timer
start with a one-shot queued invocation of attemptGrab(). Keep the existing
timer stop, state reset, update(), and scheduleRetry() behavior unchanged.

---

Nitpick comments:
In `@src/network/SessionService.cpp`:
- Around line 409-412: In the session-loading completion flow, move the
m_bulkRevokeRefreshPending check and revokeLoadedOtherDevices() call to after
emit sessionsChanged() and emit sessionsLoaded(). Preserve clearing the pending
flag before triggering the revocation so all refreshed-session signals are
delivered first.

In `@tests/contracts/generate_jellyfin_openapi_manifest.py`:
- Around line 55-65: Update the parameter collection in the manifest generator
to deduplicate entries by the `(name, in)` pair, processing path-level
parameters before operation-level parameters so later operation entries replace
matching path entries. Ensure the final manifest contains only one entry per
pair and does not use name alone as the identity.
- Around line 120-159: Add a Nix test in nix/tests.nix that fetches the pinned
Jellyfin OpenAPI artifact with its fixed hash, runs
generate_jellyfin_openapi_manifest.py using the pinned contracts, writes the
result to a temporary path, and compares it byte-for-byte with
jellyfin-12-openapi-manifest.json. Keep the existing manifest validation and
ensure the check exercises main’s SHA/version validation.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8583fe25-07dc-4eee-b361-c4b423d74b9f

📥 Commits

Reviewing files that changed from the base of the PR and between dffdd24 and 03c558a.

📒 Files selected for processing (24)
  • docs/provider-compatibility.md
  • src/CMakeLists.txt
  • src/cmake/BloomLibraries.cmake
  • src/network/SessionService.cpp
  • src/network/SessionService.h
  • src/providers/jellyfin/JellyfinModelMapper.cpp
  • src/ui/ImageCacheProvider.cpp
  • src/ui/ImageCacheProvider.h
  • src/ui/ImageCacheStore.cpp
  • src/ui/ImageCacheStore.h
  • src/ui/LibraryScreen.qml
  • src/ui/SoftwareRoundedImage.cpp
  • src/ui/SoftwareRoundedImage.h
  • src/ui/SoftwareRoundedImageRegistration.h
  • tests/ArtworkRefreshTest.cpp
  • tests/CMakeLists.txt
  • tests/ProviderCatalogTest.cpp
  • tests/RoundedImageTest.cpp
  • tests/VisualRegressionTest.cpp
  • tests/contracts/generate_jellyfin_openapi_manifest.py
  • tests/contracts/jellyfin-12-openapi-manifest.json
  • tests/contracts/provider-contracts.json
  • tests/contracts/provider_contracts_test.py
  • tests/contracts/validate_contracts.py
🚧 Files skipped from review as they are similar to previous changes (9)
  • src/ui/ImageCacheStore.h
  • tests/RoundedImageTest.cpp
  • src/ui/LibraryScreen.qml
  • tests/ProviderCatalogTest.cpp
  • docs/provider-compatibility.md
  • tests/contracts/provider_contracts_test.py
  • src/ui/ImageCacheProvider.cpp
  • src/providers/jellyfin/JellyfinModelMapper.cpp
  • tests/contracts/provider-contracts.json

Comment thread src/network/SessionService.cpp
Comment thread src/network/SessionService.cpp
Comment thread src/ui/ImageCacheProvider.h Outdated
Comment thread tests/contracts/validate_contracts.py Outdated
@crowquillx
crowquillx merged commit b87caf6 into main Aug 11, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant