Skip to content

feat(sounds): add provider-backed sound library hook#4540

Merged
NotThatKindOfDrLiz merged 10 commits into
mainfrom
feature/freesound-audio-hook
May 21, 2026
Merged

feat(sounds): add provider-backed sound library hook#4540
NotThatKindOfDrLiz merged 10 commits into
mainfrom
feature/freesound-audio-hook

Conversation

@rabble
Copy link
Copy Markdown
Member

@rabble rabble commented May 19, 2026

Closes #4541

Summary

  • Add normalized external sound source and license metadata to AudioEvent
  • Add SoundLibraryApiClient for /api/sounds/providers and /api/sounds/search
  • Add Riverpod hooks for provider discovery and provider-backed sound search
  • Document the provider-backed sound library plan and constraints

Tests

  • flutter analyze lib/services/sound_library_api_client.dart lib/providers/sounds_providers.dart test/providers/sounds_providers_test.dart
  • flutter test mobile/packages/models/test/src/audio_event_test.dart
  • flutter test test/providers/sounds_providers_test.dart test/services/sound_library_api_client_test.dart
  • pre-push hook: analyzer, generated file verification, changed tests

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Member Author

@rabble rabble left a comment

Choose a reason for hiding this comment

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

Review summary

Two blocking architecture issues, otherwise the code itself is clean and well-tested. Requesting changes because both issues are direct violations of .claude/rules/ policy that future PRs in this stack will inherit if we land it as-is.


Blocking

1. New feature should be BLoC, not new Riverpod providers.

mobile/lib/providers/sounds_providers.dart adds three brand-new Riverpod providers (soundLibraryApiClientProvider, soundLibraryProvidersProvider, soundLibrarySearchProvider) for a brand-new feature surface. .claude/rules/state_management.md "Migration Policy" is unambiguous:

New features: Use flutter_bloc following the layered architecture (UI → BLoC → Repository → Client)
Existing code: Riverpod is used for legacy code maintenance only

The fact that the existing sounds_providers.dart file already uses Riverpod isn't a license to extend it — that file is legacy. The "hook" in the PR title is doing what a SoundLibraryBloc / SoundLibrarySearchCubit should do (load providers, run a parameterized search). Suggest replacing both FutureProviders with a Cubit emitting a SoundLibrarySearchState (status enum + result list + license metadata).

2. Composition / fallback / provider routing belongs in a repository, not in the client.

.claude/rules/architecture.md:

Fallback and composition logic belongs in the repository — when data can come from multiple sources (e.g., try API first, fall back to local cache or relay), the repository decides the strategy. BLoCs and UI should never implement source-selection or fallback logic.

The design doc explicitly lists divine, nostr, freesound, openverse as providers, and the nostr provider is specced to be "backed by Funnelcake/Nostr reads." That's composition across data sources — the canonical repository responsibility. A SoundsRepository already exists in mobile/packages/sounds_repository/ and is the natural home; the new SoundLibraryApiClient should be one of its data clients, not something the UI talks to directly via a FutureProvider.family. Without this, the eventual nostr provider routing will have to land either in the UI or in the client (which is just an HTTP wrapper today) — both wrong layers.

Concretely: add a method like SoundsRepository.searchProviderSounds(SoundLibrarySearchRequest) that dispatches to SoundLibraryApiClient for external providers and to the existing Nostr path for nostr, and have the new bloc consume the repository.


Nits

  • Mixed provider declaration styles in one file. Hand-written final Provider<X> = Provider<X>((ref) => ...) sits next to @Riverpod(keepAlive: true) codegen providers (soundsRepository, trendingSounds, etc.). The // ignore: specify_nonobvious_property_types on soundLibrarySearchProvider is a smell that codegen was the right path. If Riverpod is kept (against finding #1), use @riverpod consistently.
  • SoundLibrarySearchRequest re-implements == / hashCode by hand. Five fields, all primitives — Equatable or a record would be shorter and harder to get wrong when fields are added.
  • SoundLibraryApiException implements Exception but the rule in error_handling.md is "use descriptive exception classes." Good. But consider whether the code strings (invalid_query, provider_disabled, etc.) should be a typed enum so callers can switch on them — string-typed error codes drift between client and proxy.
  • _soundFromJson does unchecked as String casts on provider / providerId / previewUrl / license. If the proxy ever returns a malformed result (or a partially-disabled provider), this throws TypeError mid-list-map and the whole search fails. Either validate fields explicitly and skip bad rows (and log), or surface as SoundLibraryApiException so the BLoC can addError it cleanly.
  • Timeout (12s) and base URL are constructor-injectable but the production soundLibraryApiClientProvider builds with defaults. That's fine for now but worth a // TODO(#issue): if the value is expected to be tuned, per code_style.md "Temporary Code" — or move to a SoundLibraryApiConfig constant per "No Hardcoded Values."

Things checked and OK

  • AudioEvent / AudioExternalSource / AudioLicenseMetadata model additions look clean: JSON round-trip is tested, copyWith is updated, toJson omits the new field when null. License is required on AudioExternalSource which matches the proxy contract ("Every returned result must include normalized license metadata or be dropped").
  • No Riverpod-bridge identity-capture bug here. SoundLibraryApiClient has no auth-flippable dependencies; _baseUri/_timeout are immutable post-construction. The BlocProvider-keyed-on-identity rule does not apply.
  • Test coverage on the new client and providers is reasonable — MockClient exercises both happy and error paths; provider tests use overrideWithValue. (Once finding #1 is addressed, these need to migrate to blocTest.)
  • No l10n / theming / divine_ui issues since this PR is API + model only, no UI yet.

Verdict

Request changes. The two blocking issues are policy violations that get harder to undo once UI lands on top of them — better to flip Riverpod → BLoC and insert the repository layer now, before the audio-picker integration arrives.

rabble added a commit that referenced this pull request May 19, 2026
…ition

Addresses self-review on PR #4540:

- Move SoundLibraryApiClient into the sounds_repository package and
  inject baseUri so the package stays Flutter-free.
- Extend SoundsRepository with searchExternalLibrary / fetchExternalProviders
  so source-selection (divine/nostr/freesound/openverse) lives at the
  repository layer per architecture.md, not in the BLoC or UI.
- Replace soundLibraryApiClientProvider / soundLibraryProvidersProvider /
  soundLibrarySearchProvider Riverpod providers with SoundLibraryBloc,
  using enum status + addError (no error strings in state) and restartable
  query handler so stale searches are cancelled.
- Harden _soundFromJson against malformed proxy rows: required string
  fields and missing license surface as SoundLibraryApiException instead
  of an opaque TypeError that would abort the whole search.
- Add bloc tests, repository routing tests, and a malformed-row regression
  test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@rabble rabble force-pushed the feature/freesound-audio-hook branch from 6a9f640 to 5a4a62d Compare May 19, 2026 23:12
@rabble
Copy link
Copy Markdown
Member Author

rabble commented May 19, 2026

Addressed self-review feedback in 5a4a62d:

  • Moved data-source composition into SoundsRepository (searchExternalLibrary, fetchExternalProviders); UI/BLoC no longer touches SoundLibraryApiClient directly. Client lives in the sounds_repository package.
  • Replaced the 3 new FutureProviders with SoundLibraryBloc (enum-status state, no error strings, addError, restartable() query, droppable() pagination, no mutable instance vars).
  • Hardened _soundFromJson — malformed rows surface as SoundLibraryApiException instead of TypeError. Added regression tests.

Rebased onto fresh origin/main; resolved the audio_event.dart conflict by keeping both upstream's fromLocalImport / localImportMarker / isLocalImport / localFilePath additions (from #4503) and this PR's externalSource / externalProviderMarker / isExternalProviderSound / AudioExternalSource additions — both sides are independent feature additions, so neither was dropped.

@github-actions

This comment has been minimized.

@rabble
Copy link
Copy Markdown
Member Author

rabble commented May 20, 2026

CI fix in 82d0cd8:

  • Models format (Flutter 3.41.9): applied the pinned formatter to 6 upstream files (aspect_ratio, curation_set, divine_filter, feed_type, logging_types, nostr_app_audit_event). No semantic changes — these were just stricter under the version this PR pinned.
  • sounds_repository coverage 88.97% → 100%: added tests for SoundLibraryApiClient covering default constructor branch, ClientException wrapping, non-JSON success body, non-JSON / non-Map error body, empty query rejection, TypeError from AudioLicenseMetadata.fromJson surfacing as a typed exception, the openverse provider label, count fallback when absent, invalid search/provider response shapes, blank license_type omission. Also added value-equality / hashCode / toString tests for SoundLibraryProviderInfo, SoundLibrarySearchRequest, and SoundLibraryApiException. No // coverage:ignore-line used — every reported uncovered line is now covered by a test.

Verified locally on Flutter 3.41.9 via mise exec:

  • cd mobile/packages/models && dart format --set-exit-if-changed lib test exits 0 (92 files, 0 changed)
  • cd mobile/packages/sounds_repository && very_good test --coverage --min-coverage 100 --report-on lib --show-uncovered exits 0 (63 tests passing)
  • cd mobile && flutter analyze lib test integration_test reports no issues

@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Member Author

@rabble rabble left a comment

Choose a reason for hiding this comment

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

Re-review of 5a4a62d30 + 82d0cd82f — all three prior blockers resolved.

Riverpod → BLoC migration ✓. mobile/lib/blocs/sound_library/sound_library_bloc.dart ships SoundLibraryBloc with sealed events, enum-status state (SoundLibraryProvidersStatus, SoundLibrarySearchStatus), restartable() on QueryChanged, droppable() on PageRequested, errors via addError() (no error strings/exceptions in state), no mutable instance vars. Old Riverpod hooks fully removed.

Composition moved to repository ✓. SoundLibraryApiClient now lives in mobile/packages/sounds_repository/lib/src/sound_library_api_client.dart (Flutter-free: http + meta only). SoundsRepository.fetchExternalProviders() and .searchExternalLibrary(SoundLibrarySearchRequest) are the BLoC's only surface — client is internal, matching architecture.md.

_soundFromJson hardened ✓. New _requireString helper throws SoundLibraryApiException; the AudioLicenseMetadata.fromJson TypeError is caught and rewrapped with a single annotated // ignore: avoid_catching_errors. Regression test added.

Forward-looking concerns (non-blocking):

  • When the UI PR lands, the consuming widget must ref.watch(soundsRepositoryProvider) and put it in a ValueKey on BlocProvider<SoundLibraryBloc> per state_management.md "Bridging Riverpod-provided dependencies into BlocProvider." Worth pinning in the design doc.
  • pubkey: AudioEvent.externalProviderMarker ('external_provider') stuffs a sentinel into a Nostr-pubkey field — anything calling npub.encode(pubkey) will choke. Cleaner shape would be a nullable pubkey or an origin enum. Flag before any UI reads pubkey of external sounds.
  • Formatter churn in 6 unrelated models/ files (from pinning Flutter 3.41.9) is mechanically correct but ideally would be a separate chore(models) PR per code_style.md "PR Scope". Acceptable here since the pin lives in this PR.

CI green, sounds_repository at 100% coverage. Verdict: LGTM (posted as comment — GitHub blocks --approve on own PRs).

rabble and others added 10 commits May 21, 2026 17:37
…ition

Addresses self-review on PR #4540:

- Move SoundLibraryApiClient into the sounds_repository package and
  inject baseUri so the package stays Flutter-free.
- Extend SoundsRepository with searchExternalLibrary / fetchExternalProviders
  so source-selection (divine/nostr/freesound/openverse) lives at the
  repository layer per architecture.md, not in the BLoC or UI.
- Replace soundLibraryApiClientProvider / soundLibraryProvidersProvider /
  soundLibrarySearchProvider Riverpod providers with SoundLibraryBloc,
  using enum status + addError (no error strings in state) and restartable
  query handler so stale searches are cancelled.
- Harden _soundFromJson against malformed proxy rows: required string
  fields and missing license surface as SoundLibraryApiException instead
  of an opaque TypeError that would abort the whole search.
- Add bloc tests, repository routing tests, and a malformed-row regression
  test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This PR pinned the models CI Flutter version to 3.41.9, whose
`dart format` is stricter than what `origin/main` shipped with. Six
upstream files needed reformatting to make
`dart format --set-exit-if-changed lib test` exit 0 under the pinned
version. No semantic changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bring SoundLibraryApiClient line coverage from 88.97% to 100% so the
package CI's --min-coverage 100 gate passes. Adds tests for: default
constructor (auto-instantiated http.Client / timeout), wrapped
ClientException, non-JSON success body, non-JSON error body, error body
that decodes to a non-Map, empty query rejection, TypeError from
license metadata parsing surfacing as a typed API exception, the
openverse provider label branch, count-fallback when absent, invalid
search/provider response shapes, blank license_type omission. Also adds
value-equality, hashCode, and toString coverage for
SoundLibraryProviderInfo, SoundLibrarySearchRequest, and
SoundLibraryApiException.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@NotThatKindOfDrLiz NotThatKindOfDrLiz force-pushed the feature/freesound-audio-hook branch from 82d0cd8 to 837da32 Compare May 21, 2026 22:40
@NotThatKindOfDrLiz
Copy link
Copy Markdown
Member

Pushed a cleanup pass onto feature/freesound-audio-hook.

What changed:

  • removed the unshipped SoundLibraryBloc slice and its bloc test so the PR no longer lands dead app-layer code
  • removed the ungated production SoundLibraryApiClient wiring from soundsRepositoryProvider; the app stays on the existing sound path until a real UI rollout is ready
  • kept the package/model groundwork for external sound metadata and added value equality coverage for AudioExternalSource and AudioLicenseMetadata
  • pinned .github/workflows/sounds_repository.yaml to Flutter 3.41.9 to match the models CI pin already introduced in this PR
  • rebased the branch onto fresh origin/main

Local verification after rebase:

  • flutter analyze lib test integration_test
  • flutter test packages/models/test/src/audio_event_test.dart
  • flutter test test/providers/sounds_providers_test.dart
  • flutter test from mobile/packages/sounds_repository

I’m watching CI on this head and will fix anything that comes back red.

@github-actions
Copy link
Copy Markdown

Mobile PR Preview

Preview refreshed for 837da32

Last refresh: 837da32 at 2026-05-21 22:44:21 UTC (preview run)

Property Value
Preview URL https://b9df2f80.openvine-app.pages.dev
Pages project openvine-app
Preview branch pr-4540
PR branch feature/freesound-audio-hook
Commit 837da32

Copy link
Copy Markdown
Member

@NotThatKindOfDrLiz NotThatKindOfDrLiz left a comment

Choose a reason for hiding this comment

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

Final pass complete after cleanup. Removed the unshipped app-layer sound library wiring, kept the repository/model groundwork, added value-equality coverage, aligned the sounds repository CI pin with models CI, rebased onto current main, and reran the relevant local verification. Current GitHub checks are green and this is ready to merge.

@NotThatKindOfDrLiz NotThatKindOfDrLiz merged commit 9140727 into main May 21, 2026
12 checks passed
@NotThatKindOfDrLiz NotThatKindOfDrLiz deleted the feature/freesound-audio-hook branch May 21, 2026 22:47
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.

feat(sounds): add provider-backed sound library hook

2 participants