Skip to content

Propagate album/artist renames across providers#197

Merged
phanan merged 5 commits into
masterfrom
feat/propagate-renames-across-providers
May 3, 2026
Merged

Propagate album/artist renames across providers#197
phanan merged 5 commits into
masterfrom
feat/propagate-renames-across-providers

Conversation

@phanan
Copy link
Copy Markdown
Member

@phanan phanan commented May 2, 2026

Summary

Songs hold denormalised album/artist names (albumName, artistName, albumArtistName) and albums hold a denormalised artistName for performance. When the user edits an album or artist's name, those copies were left stale until the next paginate or relogin — list rows kept showing the old name.

This PR fans the rename out across providers via two new static broadcast streams that mirror the existing AuthProvider.userLoggedOutStream pattern:

  • AlbumProvider.renamedStream — fires from update() when the server-returned name differs.
  • ArtistProvider.renamedStream — same shape.

PlayableProvider subscribes to both:

  • on album rename: songs where song.albumId == album.id get albumName updated.
  • on artist rename: songs where song.artistId == artist.id get artistName updated; songs where song.albumArtistId == artist.id get albumArtistName updated. The two checks are independent because a featured-track song's primary artist can differ from its album-artist.

AlbumProvider also subscribes to ArtistProvider.renamedStream and updates album.artistName for albums whose artistId matches.

Test plan

  • flutter test — all 380 tests pass, including test/providers/rename_propagation_test.dart (5 new):
    • songs sharing the renamed album get their albumName updated
    • no propagation when the album name didn't actually change
    • songs whose artist matches get artistName updated (and albumArtistName when the album-artist matches)
    • songs where albumArtistId matches but artistId does not get only albumArtistName updated
    • albums sharing the renamed artist get their artistName updated
  • Manual: rename an album and an artist on a fresh-master koel; navigate between Songs / Albums / Artists screens — the new name shows everywhere without a refresh.

Summary by CodeRabbit

  • Bug Fixes

    • Rename changes now reliably propagate across albums, songs, and displayed subtitles, and notifications occur only when data actually changes.
  • Refactor

    • Multiple UI widgets (SongCard, AlbumCard, ArtistCard, PlayableRow, PlayableInfo, Album/Artist details) now rebuild in response to provider updates so on-screen text stays in sync.
  • Documentation

    • Added repository coding guidelines.
  • Tests

    • New tests cover rename propagation and widget subtitle updates.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 2, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Providers now emit rename events: ArtistProvider and AlbumProvider publish broadcast renamedStreams when names change. AlbumProvider subscribes to artist renames to update cached albums; PlayableProvider subscribes to both album and artist renames to update cached songs. Tests verify propagation and UI widgets rebuild.

Changes

Rename propagation (single cohesive DAG)

Layer / File(s) Summary
Stream declarations
lib/providers/artist_provider.dart, lib/providers/album_provider.dart
Adds private static broadcast StreamControllers and public static renamedStream getters for Artist and Album.
Emit on update
lib/providers/artist_provider.dart, lib/providers/album_provider.dart
update() computes a renamed boolean by comparing old vs response name, updates entity fields, calls notifyListeners(), and emits the entity on its renamedStream only when renamed.
Subscriptions / Wiring
lib/providers/album_provider.dart, lib/providers/playable_provider.dart
AlbumProvider subscribes to ArtistProvider.renamedStream. PlayableProvider subscribes to AlbumProvider.renamedStream and ArtistProvider.renamedStream in its constructor; existing logout clearing remains.
Cache update handlers
lib/providers/album_provider.dart, lib/providers/playable_provider.dart
Handlers iterate cached albums/songs and update denormalized name fields when IDs match and the name differs. notifyListeners() is invoked only when at least one cached item changed.
Provider-driven UI wiring
lib/ui/widgets/song_card.dart, lib/ui/widgets/album_card.dart, lib/ui/widgets/artist_card.dart, lib/ui/widgets/now_playing/playable_info.dart, lib/ui/screens/album_details.dart, lib/ui/screens/artist_details.dart
Multiple widgets now wrap their subtree in Consumer<...Provider> so UI rebuilds when corresponding providers notify.
Refactors: subtitle and helpers
lib/ui/widgets/playable_row.dart, lib/ui/widgets/now_playing/playable_info.dart
Moved subtitle computation into private helper (_subtitle()/getter) to centralize logic.
Tests: propagation & widget rebuilds
test/providers/rename_propagation_test.dart, test/ui/widgets/*_test.dart
Adds provider-level tests for rename propagation and notification semantics; updates widget tests to pump widgets under provider harnesses and adds tests asserting UI rebuilds on provider notifications.
Repo guidelines
CLAUDE.md
Adds repository coding/PR/testing guidelines document.

Sequence Diagram

sequenceDiagram
    participant Client
    participant ArtistProvider
    participant ArtistStream as "Artist\nrenamedStream"
    participant AlbumProvider
    participant AlbumStream as "Album\nrenamedStream"
    participant PlayableProvider

    Client->>ArtistProvider: update(artist, name: "New Name")
    ArtistProvider->>ArtistProvider: detect name change\nupdate fields\nnotifyListeners()
    ArtistProvider->>ArtistStream: emit(renamedArtist)
    ArtistStream->>AlbumProvider: onArtistRenamed(artist)
    AlbumProvider->>AlbumProvider: update cached albums' artistName\nnotifyListeners() if changed
    AlbumProvider->>AlbumStream: emit(renamedAlbum) when album renamed
    AlbumStream->>PlayableProvider: onAlbumRenamed(album)
    PlayableProvider->>PlayableProvider: update songs' albumName\nnotifyListeners() if changed
    ArtistStream->>PlayableProvider: onArtistRenamed(artist)
    PlayableProvider->>PlayableProvider: update songs' artistName & albumArtistName\nnotifyListeners() if changed
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰
I hop on streams where names take flight,
Artist whispers ripple through the night.
Albums learn and songs align,
Small hops make denormalized fields shine.
A tidy garden of names, all right!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Propagate album/artist renames across providers' accurately and concisely summarizes the main objective of the changeset—introducing stream-based propagation of album and artist renames to keep denormalized fields in other providers synchronized.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/propagate-renames-across-providers

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
test/providers/rename_propagation_test.dart (1)

14-21: 💤 Low value

Simplify the casting chain.

The intermediate casts are unnecessary since List<Song> is a subtype of List<Playable> and syncWithVault accepts dynamic.

♻️ Proposed simplification
 PlayableProvider _seedPlayables(List<Song> songs) {
   final provider = PlayableProvider();
-  provider.syncWithVault(songs.cast<dynamic>().toList()
-      .map((s) => s as dynamic)
-      .toList()
-      .cast<Song>());
+  provider.syncWithVault(songs);
   return provider;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/providers/rename_propagation_test.dart` around lines 14 - 21, The
_seedPlayables function contains an unnecessary casting chain when calling
PlayableProvider.syncWithVault; remove the intermediate casts and pass the
original songs list directly. Locate the _seedPlayables function and replace the
provider.syncWithVault(...) call with a direct call using songs (e.g.,
provider.syncWithVault(songs)) so PlayableProvider and syncWithVault are used
without redundant casts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@test/providers/rename_propagation_test.dart`:
- Around line 14-21: The _seedPlayables function contains an unnecessary casting
chain when calling PlayableProvider.syncWithVault; remove the intermediate casts
and pass the original songs list directly. Locate the _seedPlayables function
and replace the provider.syncWithVault(...) call with a direct call using songs
(e.g., provider.syncWithVault(songs)) so PlayableProvider and syncWithVault are
used without redundant casts.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 18a1b21a-1a0c-4778-97cf-08abaf1b4355

📥 Commits

Reviewing files that changed from the base of the PR and between 153af44 and 1e993ec.

📒 Files selected for processing (4)
  • lib/providers/album_provider.dart
  • lib/providers/artist_provider.dart
  • lib/providers/playable_provider.dart
  • test/providers/rename_propagation_test.dart

phanan added 2 commits May 2, 2026 18:04
Songs hold denormalised album/artist names (`albumName`,
`artistName`, `albumArtistName`) and albums hold a denormalised
`artistName` for performance. When the user edits an album or
artist's name, those copies were left stale until the next paginate
or relogin — list rows kept showing the old name.

Add static broadcast streams `AlbumProvider.renamedStream` and
`ArtistProvider.renamedStream` (mirroring the existing
`AuthProvider.userLoggedOutStream` pattern). Each `update()` emits
on its stream when the server-returned name actually differs.

PlayableProvider subscribes to both:
 - on album rename: songs where `song.albumId == album.id` get
   `albumName` updated.
 - on artist rename: songs where `song.artistId == artist.id` get
   `artistName` updated; songs where `song.albumArtistId == artist.id`
   get `albumArtistName` updated. The two checks are independent
   because a featured-track song's primary artist can differ from
   its album-artist.

AlbumProvider also subscribes to `ArtistProvider.renamedStream` and
updates `album.artistName` for albums whose `artistId` matches.
@phanan phanan force-pushed the feat/propagate-renames-across-providers branch from 1afbd84 to 3cee9f9 Compare May 2, 2026 16:04
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/ui/widgets/playable_row.dart`:
- Line 104: The row is being remounted because of key: UniqueKey() in
PlayableRow; remove UniqueKey() and replace it with a stable key so the widget
rebuilds instead of remounting (e.g., use a ValueKey based on the playables
unique identifier or omit the key entirely) to preserve PlayableRowThumbnail and
its stream subscriptions during PlayableProvider updates; update the
instantiation in PlayableRow to use a stable key (for example
ValueKey(playable.id) or similar stable identifier) rather than UniqueKey().

In `@test/ui/widgets/song_card_test.dart`:
- Around line 16-28: The PlayableProvider created in _harness is never disposed
and leaks stream listeners; fix by registering disposal with the test harness:
after creating provider in _harness(WidgetTester tester, Playable playable) call
tester.addTearDown(() => provider.dispose()); this ensures the PlayableProvider
instance used by SongCard is cleaned up between tests (alternatively ensure the
returned _Harness disposes the provider in its dispose method if you prefer
encapsulation).
🪄 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: 2c4cb82c-72be-4f3f-8e68-7bf97350957d

📥 Commits

Reviewing files that changed from the base of the PR and between 3cee9f9 and a3eb11e.

📒 Files selected for processing (3)
  • lib/ui/widgets/playable_row.dart
  • lib/ui/widgets/song_card.dart
  • test/ui/widgets/song_card_test.dart

Comment thread lib/ui/widgets/playable_row.dart Outdated
Comment thread test/ui/widgets/song_card_test.dart
Earlier patch only wrapped SongCard / PlayableRow. Top Albums and Top
Artists on the home screen, the album/artist details headers, and the
now-playing subtitle were still reading mutable name fields without
observing the provider, so they kept showing the pre-rename value.

Wraps AlbumCard, ArtistCard, PlayableInfo, and the album/artist
details FutureBuilders in the matching Consumer. Updates the card
tests to provide the right ChangeNotifierProvider and add a rebuild-
on-notify case for each.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
lib/ui/widgets/now_playing/playable_info.dart (2)

12-16: 💤 Low value

Use a local variable to enable type promotion and drop the redundant explicit casts.

Because playable is an instance field, Dart cannot promote its type after an is check, requiring the redundant as cast on every branch. Assigning to a local variable first gives the compiler enough information to smart-cast.

♻️ Suggested refactor
  String get _subtitle {
-   if (playable is Song) return (playable as Song).artistName;
-   if (playable is Episode) return (playable as Episode).podcastTitle;
+   final p = playable;
+   if (p is Song) return p.artistName;
+   if (p is Episode) return p.podcastTitle;
    return '';
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ui/widgets/now_playing/playable_info.dart` around lines 12 - 16, The
_subtitle getter uses `playable` (an instance field) with `is` checks but still
uses redundant `as` casts; to enable type promotion and remove casts, assign
`playable` to a local final variable (e.g., `final p = playable`) at the start
of the `_subtitle` getter and then use `if (p is Song) return p.artistName; if
(p is Episode) return p.podcastTitle;` so the compiler can smart-cast and the
explicit `as` casts can be removed.

21-21: 💤 Low value

Prefer descriptive names over _ / __ / ___ for Consumer callback params.

All three parameters are discarded, but the learning for this repo explicitly calls out callback params as needing names that say what they are. Naming them even while unused makes the signature self-documenting and consistent with the project style.

♻️ Suggested rename
-      builder: (_, __, ___) => Column(
+      builder: (context, provider, child) => Column(

Based on learnings: "For callback params, destructured fields, lambda args, and other uses, pick a name that says what it is."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ui/widgets/now_playing/playable_info.dart` at line 21, The Consumer
builder callback currently uses `_`, `__`, `___`; rename these to descriptive
parameter names that match the Consumer signature (e.g., `context`, `value`,
`child` or `context`, `watch`, `child` depending on which provider API is used)
in the `builder: (_, __, ___) => Column(...)` expression inside
playable_info.dart so the signature is self-documenting and consistent with
project style; update any internal references if used and leave unused params
named (not `_`) to document their roles.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@lib/ui/widgets/now_playing/playable_info.dart`:
- Around line 12-16: The _subtitle getter uses `playable` (an instance field)
with `is` checks but still uses redundant `as` casts; to enable type promotion
and remove casts, assign `playable` to a local final variable (e.g., `final p =
playable`) at the start of the `_subtitle` getter and then use `if (p is Song)
return p.artistName; if (p is Episode) return p.podcastTitle;` so the compiler
can smart-cast and the explicit `as` casts can be removed.
- Line 21: The Consumer builder callback currently uses `_`, `__`, `___`; rename
these to descriptive parameter names that match the Consumer signature (e.g.,
`context`, `value`, `child` or `context`, `watch`, `child` depending on which
provider API is used) in the `builder: (_, __, ___) => Column(...)` expression
inside playable_info.dart so the signature is self-documenting and consistent
with project style; update any internal references if used and leave unused
params named (not `_`) to document their roles.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4fd10176-e280-4cbe-8901-2a91748b5af3

📥 Commits

Reviewing files that changed from the base of the PR and between a3eb11e and 387f8ca.

📒 Files selected for processing (7)
  • lib/ui/screens/album_details.dart
  • lib/ui/screens/artist_details.dart
  • lib/ui/widgets/album_card.dart
  • lib/ui/widgets/artist_card.dart
  • lib/ui/widgets/now_playing/playable_info.dart
  • test/ui/widgets/album_card_test.dart
  • test/ui/widgets/artist_card_test.dart

@phanan phanan merged commit fc47ca8 into master May 3, 2026
2 checks passed
@phanan phanan deleted the feat/propagate-renames-across-providers branch May 3, 2026 13:46
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