Skip to content

Cached fetched lyrics on disk so repeat lookups load instantly - #169

Merged
TobiasLaross merged 2 commits into
mainfrom
lyrics-cache
Jun 29, 2026
Merged

Cached fetched lyrics on disk so repeat lookups load instantly#169
TobiasLaross merged 2 commits into
mainfrom
lyrics-cache

Conversation

@TobiasLaross

@TobiasLaross TobiasLaross commented Jun 29, 2026

Copy link
Copy Markdown
Owner

What

Follow-up to the lyrics work (#168): lyrics now persist in a cache so a re-opened or
replayed track loads instantly — no network round-trip, even across app launches.

Before this, only the currently playing track was latched (lyricsTrackKey); flipping
to a song viewed earlier refetched from the providers.

How

  • New LyricsCache (Services/LyricsCache.swift): an actor holding an in-memory map
    backed by a single JSON file in the Caches directory.
    • Keyed by a normalized title + artist + album (lowercased/trimmed), so trivial
      case/spacing differences don't fragment the cache.
    • LRU-capped (200 tracks) to bound disk/memory.
    • Only successful lookups are stored — a .notFound stays retryable, matching the
      existing fetch-side latch.
    • All file I/O is best-effort (try?); a missing/corrupt file just starts empty.
  • LyricsApiService consults the cache before the provider chain and stores the result
    after; injectable so tests/previews use a memory-only cache (directory: nil).
  • Made LyricsResult / LyricLine Codable for persistence.

Tests

  • LyricsCacheTests — store/return, .notFound not cached, key normalization, LRU
    eviction, and persistence across instances (relaunch).
  • LyricsApiServiceTests — a second lookup is served from the cache even after the
    providers start missing; existing tests use a memory-only cache.
  • Full suite green.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added persistent lyrics caching, so successful lookups can be reused across app launches.
    • Improved lyrics lookup reliability by keeping cached results available even if later network requests fail.
  • Bug Fixes

    • Prevented failed lyric searches from being stored as permanent misses, allowing retries later.
    • Added support for saving and loading cached lyrics data without disrupting the app if cache data is missing or invalid.

Added a persistent LyricsCache (JSON in the Caches dir, LRU-capped) keyed by
normalized title+artist+album. Successful lookups are served from the cache on a
re-open or after relaunch without hitting the providers; misses stay retryable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011q78vsxxcRkYYvN61cCjhK
@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@TobiasLaross, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 52 minutes

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable usage-based reviews in Billing to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information, and refer to the rate limits docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 45e0358d-d490-4c29-9299-dc0f9866851d

📥 Commits

Reviewing files that changed from the base of the PR and between 88fcd54 and 54a3000.

📒 Files selected for processing (2)
  • IntelliNest/Services/LyricsCache.swift
  • IntelliNestTests/LyricsApiServiceTests.swift
📝 Walkthrough

Walkthrough

Adds a new LyricsCache Swift actor implementing a persistent, LRU-evicted cache for lyrics results stored as JSON on disk. LyricLine and LyricsResult gain Codable conformance to support serialization. LyricsApiService is updated to check and populate the cache around provider network calls. Tests cover cache behavior and service-level cache hits.

Changes

Persistent LRU Lyrics Cache

Layer / File(s) Summary
Codable conformances and project wiring
IntelliNest/Model/LyricsTimeline.swift, IntelliNest.xcodeproj/project.pbxproj
LyricLine and LyricsResult gain Codable conformance for JSON serialization; LyricsCache.swift is registered as a compiled source in the Xcode project.
LyricsCache actor
IntelliNest/Services/LyricsCache.swift
New actor LyricsCache with LRU state, normalized key generation, lazy disk load on first access, value(forKey:) and insert(_:forKey:) operations (skipping .notFound), LRU touch/evict helpers, and best-effort JSON persistence via a private StoredEntry: Codable wrapper.
LyricsApiService cache integration
IntelliNest/Services/LyricsApiService.swift
Adds cache: LyricsCache stored property and initializer parameter; fetchLyrics computes a cache key, returns cached results immediately when present, and inserts network lookup results into the cache.
Tests
IntelliNestTests/LyricsApiServiceTests.swift
Injects memory-only LyricsCache(directory: nil) into service tests; adds testSecondLookupIsServedFromCacheWithoutRefetching; introduces LyricsCacheTests covering hits, .notFound non-caching, key normalization, LRU eviction, and disk persistence across instances.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • TobiasLaross/IntelliNest#167: Modifies LyricsTimeline.swift and LyricsApiService.swift — the same files that gain Codable conformances and cache integration in this PR.
  • TobiasLaross/IntelliNest#168: Modifies LyricsApiService.swift's provider fallback chain with a shared deadline, which this PR builds on top of to add caching.

Poem

🐇 A cache full of lyrics, stored snug on the disk,
LRU order keeps old songs at risk.
.notFound won't linger, misses stay free,
JSON persists tunes for eternity.
No more re-fetching the chorus I know—
the rabbit hops fast when the cache steals the show! 🎵

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding a persistent on-disk lyrics cache for faster repeat lookups.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch lyrics-cache

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.

@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: 2

🧹 Nitpick comments (1)
IntelliNestTests/LyricsApiServiceTests.swift (1)

117-123: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the cached lookup makes zero provider requests.

Changing stubs to misses proves the result survives, but a network-first implementation that falls back to cache could still pass. Add an inverted request expectation around the second fetch.

Suggested test tightening
         // The provider now misses; a cached track must still resolve from the cache
         // rather than fall through to the (now empty) network result.
         stub(lrclibGetURL(duration: 233), json: "{}", statusCode: 404)
         stub(lrclibSearchURL(), json: "[]")
         stub(lyricsOvhURL(), json: "{}", statusCode: 404)
+        let unexpectedRequest = expectation(description: "cached lookup should not hit providers")
+        unexpectedRequest.isInverted = true
+        let providerURLs = Set([lrclibGetURL(duration: 233), lrclibSearchURL(), lyricsOvhURL()])
+        URLProtocolStub.observerRequests { request in
+            if let url = request.url, providerURLs.contains(url) {
+                unexpectedRequest.fulfill()
+            }
+        }
         let second = await service.fetchLyrics(title: title, artist: artist, album: nil, durationSeconds: 233)
         XCTAssertEqual(second, first, "a cached hit is served without re-fetching")
+        await fulfillment(of: [unexpectedRequest], timeout: 0.2)
🤖 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 `@IntelliNestTests/LyricsApiServiceTests.swift` around lines 117 - 123, The
second-fetch cache test in LyricsApiServiceTests should verify there are zero
provider requests, not just that the cached result matches. Tighten the test
around fetchLyrics(title:artist:album:durationSeconds:) by adding an inverted
request expectation for the lrclibGetURL, lrclibSearchURL, and lyricsOvhURL
stubs before calling the second fetch, so a network-first fallback cannot still
pass. Keep the existing cached equality assertion to confirm the returned value
still comes from the cache.
🤖 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 `@IntelliNest/Services/LyricsCache.swift`:
- Around line 104-112: The injected cache path in LyricsCache.persist() can fail
silently because the parent directory may not exist before writing the encoded
data. Update the persist flow in LyricsCache, ideally right after resolving
fileURL and before data.write, to create the target directory (using the
injected directory URL) if needed, then proceed with encoding and atomic write.
Keep the fix localized to persist() so the injected-directory path behaves the
same as the default cache path.

In `@IntelliNestTests/LyricsApiServiceTests.swift`:
- Around line 152-155: The test setup in setUpWithError for
LyricsApiServiceTests uses a fixed lyrics-cache-tests temp path, which can
collide across overlapping runs. Change the directory creation logic to generate
a unique temporary directory per test run (for example by incorporating a UUID
or similar unique suffix) and keep the cleanup in the same setup path so the
fixture remains isolated and stable.

---

Nitpick comments:
In `@IntelliNestTests/LyricsApiServiceTests.swift`:
- Around line 117-123: The second-fetch cache test in LyricsApiServiceTests
should verify there are zero provider requests, not just that the cached result
matches. Tighten the test around
fetchLyrics(title:artist:album:durationSeconds:) by adding an inverted request
expectation for the lrclibGetURL, lrclibSearchURL, and lyricsOvhURL stubs before
calling the second fetch, so a network-first fallback cannot still pass. Keep
the existing cached equality assertion to confirm the returned value still comes
from the cache.
🪄 Autofix (Beta)

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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9d36b7eb-f233-43fb-804c-3e4ecd386e86

📥 Commits

Reviewing files that changed from the base of the PR and between 68ba641 and 88fcd54.

📒 Files selected for processing (5)
  • IntelliNest.xcodeproj/project.pbxproj
  • IntelliNest/Model/LyricsTimeline.swift
  • IntelliNest/Services/LyricsApiService.swift
  • IntelliNest/Services/LyricsCache.swift
  • IntelliNestTests/LyricsApiServiceTests.swift

Comment thread IntelliNest/Services/LyricsCache.swift
Comment thread IntelliNestTests/LyricsApiServiceTests.swift
Created the injected cache directory before persisting so an injected path can't
silently drop the cache. Gave each persistence test its own temp subdirectory
(keyed by the test name, not a runtime UUID) so fixtures can't collide.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011q78vsxxcRkYYvN61cCjhK
@TobiasLaross
TobiasLaross merged commit 68e4eab into main Jun 29, 2026
2 checks passed
@TobiasLaross
TobiasLaross deleted the lyrics-cache branch June 29, 2026 06:26
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.

1 participant