Skip to content

Add radio stations to search results#166

Merged
phanan merged 2 commits into
masterfrom
feat/search-radio-stations
Apr 2, 2026
Merged

Add radio stations to search results#166
phanan merged 2 commits into
masterfrom
feat/search-radio-stations

Conversation

@phanan
Copy link
Copy Markdown
Member

@phanan phanan commented Apr 2, 2026

Summary

  • Parse radio_stations from the search API response into SearchResult
  • Add RadioStationCard widget with logo support and fallback antenna icon
  • Display a "Radio Stations" section in the search screen via HorizontalCardScroller
  • Tapping a station card starts playback

Test plan

  • SearchResult correctly stores radio stations (3 tests)
  • RadioStationCard renders name, description, icon, and handles tap (5 tests)
  • All 187 Flutter tests pass
  • Backend ExcerptSearchTest updated to assert radio_stations in response

Summary by CodeRabbit

Release Notes

  • New Features

    • Search results now include radio stations alongside albums, artists, and podcasts
    • Added radio station cards with logo, name, optional description, and animated tap feedback
    • Tap-to-play behavior for radio stations (default play action when tapped)
  • Tests

    • Added tests for radio station search results and the radio station card display and interactions

Display radio stations alongside songs, albums, artists, and podcasts
in the search screen, parsed from the API's radio_stations field.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 2, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 58ba4c4c-d520-473c-b460-ceed76e965fc

📥 Commits

Reviewing files that changed from the base of the PR and between 4b8c38d and 1d36d3c.

📒 Files selected for processing (2)
  • lib/ui/widgets/radio_station_card.dart
  • test/ui/widgets/radio_station_card_test.dart
✅ Files skipped from review due to trivial changes (1)
  • test/ui/widgets/radio_station_card_test.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/ui/widgets/radio_station_card.dart

📝 Walkthrough

Walkthrough

Adds radio station support to search: extends SearchResult with a radioStations list, parses radio_stations in SearchProvider, introduces RadioStationCard widget and UI section, exports the widget, and adds unit/widget tests.

Changes

Cohort / File(s) Summary
Search Data Model
lib/providers/search_provider.dart
Added radioStations field and constructor param to SearchResult; searchExcerpts now parses res['radio_stations'] into List<RadioStation> and includes it in returned/stored SearchResult.
Search UI Integration
lib/ui/screens/search.dart
Added _radioStations state, populated from result.radioStations; added "Radio Stations" section with horizontal card scroller and noResults when empty.
Radio Station Card Widget
lib/ui/widgets/radio_station_card.dart
New RadioStationCard StatefulWidget: rounded logo (uses CachedNetworkImage with fallback), name/optional description, tap opacity animation, default tap delegates to RadioPlayerProvider.play().
Widget Export
lib/ui/widgets/widgets.dart
Re-exported radio_station_card.dart from widgets barrel.
Tests
test/providers/search_provider_test.dart, test/ui/widgets/radio_station_card_test.dart
Added tests for SearchResult radioStations behavior and widget tests for RadioStationCard rendering (name, optional description, logo vs default icon) and tap callback handling.

Sequence Diagram

sequenceDiagram
    participant User
    participant SearchScreen
    participant SearchProvider
    participant AppState
    participant RadioStationCard
    participant RadioPlayerProvider

    User->>SearchScreen: Enter query
    SearchScreen->>SearchProvider: searchExcerpts(query)
    SearchProvider->>SearchProvider: Parse API response (includes radio_stations)
    SearchProvider->>AppState: Save SearchResult (with radioStations)
    SearchScreen->>AppState: Read updated SearchResult
    SearchScreen->>SearchScreen: Update _radioStations and rebuild UI
    SearchScreen->>RadioStationCard: Render card per station
    User->>RadioStationCard: Tap card
    RadioStationCard->>RadioPlayerProvider: play(station)
    RadioPlayerProvider->>RadioPlayerProvider: Start playback
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Add radio station support #138: Adds and wires up radio-station support, including List<RadioStation> handling and RadioStation.fromJson, closely related to these changes.

Poem

🐰
I hopped through code, ears tuned to find,
New stations seeded in search's mind.
Cards that glow and icons that hum,
Tap to play — the rabbit's drum. 📻✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add radio stations to search results' directly and clearly summarizes the main change: integrating radio station data into the search functionality across the model, UI screen, and widgets.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ 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/search-radio-stations

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 (5)
lib/ui/widgets/radio_station_card.dart (1)

47-53: Consider adding a placeholder for the loading state.

CachedNetworkImage has an errorWidget for failed loads, but no placeholder for the loading state. Users will see an empty area while the logo loads. Adding a placeholder improves perceived performance.

♻️ Suggested improvement
                    ? CachedNetworkImage(
                        imageUrl: widget.station.logo!,
                        width: _cardWidth,
                        height: _cardWidth,
                        fit: BoxFit.cover,
+                       placeholder: (_, __) => _defaultIcon(),
                        errorWidget: (_, __, ___) => _defaultIcon(),
                      )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ui/widgets/radio_station_card.dart` around lines 47 - 53, The
CachedNetworkImage usage inside the RadioStationCard currently supplies only
errorWidget and no placeholder, causing an empty area while the logo loads;
update the CachedNetworkImage (in the RadioStationCard widget where imageUrl:
widget.station.logo! is used) to include a placeholder parameter that returns a
lightweight loading UI (e.g., the same _defaultIcon() or a SizedBox with a
spinner) so users see a fallback during network fetch; ensure the placeholder is
fast and visually consistent with the errorWidget.
lib/providers/search_provider.dart (1)

40-40: Pre-existing: Search keywords are not URL-encoded.

The keywords variable is interpolated directly into the URL without encoding. Special characters like &, =, #, or spaces could break the query or cause unexpected behavior. While this is pre-existing code, consider URL-encoding for robustness.

♻️ Suggested fix
-    final res = await get('search?q=$keywords');
+    final res = await get('search?q=${Uri.encodeQueryComponent(keywords)}');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/providers/search_provider.dart` at line 40, The search URL interpolates
keywords directly into get('search?q=$keywords'), which can break on spaces or
reserved characters; update the call to URL-encode the query (use
Uri.encodeQueryComponent or build the URI with queryParameters) so the keywords
are safely encoded before calling get — change the invocation that references
keywords in search_provider.dart to use the encoded value (e.g., replace the
direct interpolation of keywords with an encoded query component or a Uri with
queryParameters).
test/ui/widgets/radio_station_card_test.dart (2)

42-51: Consider adding a test for logo image rendering.

The tests verify the default icon when no logo is present, but there's no test verifying that CachedNetworkImage is rendered when station.logo is provided. This would increase coverage of the conditional logo rendering logic.

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

In `@test/ui/widgets/radio_station_card_test.dart` around lines 42 - 51, Add a new
widget test mirroring the existing 'shows default icon when no logo' test that
sets a RadioStation with a non-null logo (use RadioStation.fake(name: ..., logo:
'https://...') or the factory field) and pumps RadioStationCard(station:
station); then assert the logo branch renders by expecting
find.byType(CachedNetworkImage) (and optionally verify the image URL via widget
inspection) to cover the conditional rendering in RadioStationCard and ensure
CachedNetworkImage is used when station.logo is provided.

31-40: Brittle assertion on Text widget count.

The assertion find.byType(Text), findsOneWidget (line 39) is fragile. If the widget hierarchy changes or the test harness adds surrounding Text widgets, this test will break unexpectedly. Consider a more targeted assertion.

♻️ Suggested alternative
-    // Only the name text widget should be present
-    expect(find.byType(Text), findsOneWidget);
+    // Description should not be rendered
+    expect(find.text('The best jazz station'), findsNothing);

This directly asserts the absence of a description rather than relying on Text widget counts.

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

In `@test/ui/widgets/radio_station_card_test.dart` around lines 31 - 40, The test
"does not render description when absent" uses a brittle assertion checking Text
widget count; instead, update this test (the testWidgets block that instantiates
RadioStationCard with RadioStation.fake) to assert explicitly that the
description string is not present (e.g., expect a finder for the expected
description text to findNothing) or use a more specific finder targeting the
description widget, rather than relying on find.byType(Text) to equal one.
lib/ui/screens/search.dart (1)

182-197: Add Feature.radioStations enum and feature-gate the radio stations UI section.

The backend defensively handles missing radio_stations (similar to podcasts with its "backward compatibility" comment), but the UI lacks feature-gating. Since podcasts are feature-gated with Feature.podcasts.isSupported() at line 165, radio stations should follow the same pattern—add a Feature.radioStations enum entry to lib/utils/features.dart with the appropriate version, then wrap the radio stations section (lines 182-197) with a feature check for consistency.

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

In `@lib/ui/screens/search.dart` around lines 182 - 197, Add a new Feature enum
member named radioStations (e.g., Feature.radioStations) in the features enum
with the appropriate introduced version number (matching how podcasts is
declared) and ensure it exposes isSupported(); then wrap the radio stations UI
block (the Heading5 'Radio Stations', the _radioStations.isEmpty check, and the
HorizontalCardScroller that builds RadioStationCard with station) in a feature
gate identical to podcasts (if (Feature.radioStations.isSupported()) { ... }) so
the section only renders when the feature is supported.
🤖 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/providers/search_provider.dart`:
- Line 40: The search URL interpolates keywords directly into
get('search?q=$keywords'), which can break on spaces or reserved characters;
update the call to URL-encode the query (use Uri.encodeQueryComponent or build
the URI with queryParameters) so the keywords are safely encoded before calling
get — change the invocation that references keywords in search_provider.dart to
use the encoded value (e.g., replace the direct interpolation of keywords with
an encoded query component or a Uri with queryParameters).

In `@lib/ui/screens/search.dart`:
- Around line 182-197: Add a new Feature enum member named radioStations (e.g.,
Feature.radioStations) in the features enum with the appropriate introduced
version number (matching how podcasts is declared) and ensure it exposes
isSupported(); then wrap the radio stations UI block (the Heading5 'Radio
Stations', the _radioStations.isEmpty check, and the HorizontalCardScroller that
builds RadioStationCard with station) in a feature gate identical to podcasts
(if (Feature.radioStations.isSupported()) { ... }) so the section only renders
when the feature is supported.

In `@lib/ui/widgets/radio_station_card.dart`:
- Around line 47-53: The CachedNetworkImage usage inside the RadioStationCard
currently supplies only errorWidget and no placeholder, causing an empty area
while the logo loads; update the CachedNetworkImage (in the RadioStationCard
widget where imageUrl: widget.station.logo! is used) to include a placeholder
parameter that returns a lightweight loading UI (e.g., the same _defaultIcon()
or a SizedBox with a spinner) so users see a fallback during network fetch;
ensure the placeholder is fast and visually consistent with the errorWidget.

In `@test/ui/widgets/radio_station_card_test.dart`:
- Around line 42-51: Add a new widget test mirroring the existing 'shows default
icon when no logo' test that sets a RadioStation with a non-null logo (use
RadioStation.fake(name: ..., logo: 'https://...') or the factory field) and
pumps RadioStationCard(station: station); then assert the logo branch renders by
expecting find.byType(CachedNetworkImage) (and optionally verify the image URL
via widget inspection) to cover the conditional rendering in RadioStationCard
and ensure CachedNetworkImage is used when station.logo is provided.
- Around line 31-40: The test "does not render description when absent" uses a
brittle assertion checking Text widget count; instead, update this test (the
testWidgets block that instantiates RadioStationCard with RadioStation.fake) to
assert explicitly that the description string is not present (e.g., expect a
finder for the expected description text to findNothing) or use a more specific
finder targeting the description widget, rather than relying on
find.byType(Text) to equal one.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2909e546-7b62-4c91-abee-484f5397530d

📥 Commits

Reviewing files that changed from the base of the PR and between a99ef2e and 4b8c38d.

📒 Files selected for processing (6)
  • lib/providers/search_provider.dart
  • lib/ui/screens/search.dart
  • lib/ui/widgets/radio_station_card.dart
  • lib/ui/widgets/widgets.dart
  • test/providers/search_provider_test.dart
  • test/ui/widgets/radio_station_card_test.dart

Add loading placeholder to RadioStationCard's CachedNetworkImage,
matching the existing pattern in AlbumArtistThumbnail. Improve test
coverage with a CachedNetworkImage rendering test and fix brittle
Text widget count assertion.
@phanan phanan merged commit 2e42226 into master Apr 2, 2026
2 checks passed
@phanan phanan deleted the feat/search-radio-stations branch April 2, 2026 20:15
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