From 1ff53d8084207a7a12fdfdf67d6185856e2133ad Mon Sep 17 00:00:00 2001 From: Caleb Allen Date: Mon, 23 Mar 2026 16:55:22 -0400 Subject: [PATCH] chore(M004/S01): auto-commit after complete-slice --- backend/app.go | 9 ++ backend/explore/explore.go | 118 ++++++++++++++++++ frontend/index.ts | 2 + .../components/explore-view/explore-view.ts | 42 +++++++ .../src/components/sidebar/app-sidebar.ts | 3 +- 5 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 backend/explore/explore.go create mode 100644 frontend/src/components/explore-view/explore-view.ts diff --git a/backend/app.go b/backend/app.go index 93bd287..c4a659c 100644 --- a/backend/app.go +++ b/backend/app.go @@ -15,6 +15,7 @@ import ( "yellowjacket/backend/config" "yellowjacket/backend/coverart" "yellowjacket/backend/database" + "yellowjacket/backend/explore" "yellowjacket/backend/frontendutil" "yellowjacket/backend/library" "yellowjacket/backend/mediacontrols" @@ -37,6 +38,7 @@ type YellowJacketApp struct { player *player.Player playlist *playlist.Service queue *queue.Queue + explore *explore.Service mediaControls mediacontrols.Handler tagWriter *tagwriter.TagWriter appContext context.Context @@ -125,6 +127,11 @@ func NewYellowJacketApp( yjApp.library, ) + // create explore service + yjApp.explore = explore.NewExploreService( + yjApp.logger.WithGroup("explore"), yjApp.database, + ) + yjApp.FEBindings = []any{ yjApp.FrontendUtil, yjApp.appConfig, @@ -133,6 +140,7 @@ func NewYellowJacketApp( yjApp.queue, yjApp.player, yjApp.tagWriter, + yjApp.explore, } return yjApp, nil @@ -179,6 +187,7 @@ func (yj *YellowJacketApp) OnStartup(ctx context.Context) { yj.player.SetContext(ctx) yj.tagWriter.SetContext(ctx) + yj.explore.SetContext(ctx) // Wire queue (created in NewYellowJacketApp for Wails binding) yj.queue.SetContext(ctx) diff --git a/backend/explore/explore.go b/backend/explore/explore.go new file mode 100644 index 0000000..7f402f9 --- /dev/null +++ b/backend/explore/explore.go @@ -0,0 +1,118 @@ +package explore + +import ( + "context" + "log/slog" + + "yellowjacket/backend/database" +) + +// Service is the Wails-bound service for the explore feature. +// It owns the lifecycle of all explore-related components: the +// MusicBrainz client, ListenBrainz client, rate limiter, and +// response cache. Its exported methods form the binding surface +// that the frontend calls via generated TypeScript stubs. +type Service struct { + mb *MusicBrainzClient + lb *ListenBrainzClient + cache *Cache + logger *slog.Logger + ctx context.Context +} + +// NewExploreService creates a Service backed by the given +// database. It instantiates the rate limiter, cache, MusicBrainz +// client, and ListenBrainz client internally. +func NewExploreService(logger *slog.Logger, db *database.DB) *Service { + cache := NewCache(db, logger.WithGroup("cache")) + limiter := NewRateLimiter() + mb := NewMusicBrainzClient(cache, logger.WithGroup("musicbrainz")) + lb := NewListenBrainzClient(limiter, cache, logger.WithGroup("listenbrainz")) + + logger.Info("explore service created") + + return &Service{ + mb: mb, + lb: lb, + cache: cache, + logger: logger, + ctx: context.Background(), + } +} + +// SetContext injects the Wails runtime context. Called from +// OnStartup after the Wails runtime is initialised. +func (e *Service) SetContext(ctx context.Context) { + e.ctx = ctx +} + +// --------------------------------------------------------------------------- +// MusicBrainz search +// --------------------------------------------------------------------------- + +// SearchArtists queries MusicBrainz for artists matching the query. +func (e *Service) SearchArtists(query string) ([]MBArtist, error) { + return e.mb.SearchArtists(e.ctx, query, 0) +} + +// SearchReleaseGroups queries MusicBrainz for release groups matching the query. +func (e *Service) SearchReleaseGroups(query string) ([]MBReleaseGroup, error) { + return e.mb.SearchReleaseGroups(e.ctx, query, 0) +} + +// SearchRecordings queries MusicBrainz for recordings matching the query. +func (e *Service) SearchRecordings(query string) ([]MBRecording, error) { + return e.mb.SearchRecordings(e.ctx, query, 0) +} + +// --------------------------------------------------------------------------- +// MusicBrainz lookup +// --------------------------------------------------------------------------- + +// LookupArtist fetches a single MusicBrainz artist by MBID. +func (e *Service) LookupArtist(mbid string) (*MBArtist, error) { + return e.mb.LookupArtist(e.ctx, mbid) +} + +// LookupReleaseGroup fetches a single MusicBrainz release group by MBID. +func (e *Service) LookupReleaseGroup(mbid string) (*MBReleaseGroup, error) { + return e.mb.LookupReleaseGroup(e.ctx, mbid) +} + +// --------------------------------------------------------------------------- +// MusicBrainz browse +// --------------------------------------------------------------------------- + +// BrowseReleaseGroups fetches release groups for a given artist MBID. +func (e *Service) BrowseReleaseGroups(artistMBID string) ([]MBReleaseGroup, error) { + return e.mb.BrowseReleaseGroups(e.ctx, artistMBID) +} + +// BrowseReleases fetches releases for a given release group MBID. +func (e *Service) BrowseReleases(releaseGroupMBID string) ([]MBRelease, error) { + return e.mb.BrowseReleases(e.ctx, releaseGroupMBID) +} + +// --------------------------------------------------------------------------- +// ListenBrainz +// --------------------------------------------------------------------------- + +// TopRecordingsForArtist returns the most-listened recordings for an artist. +func (e *Service) TopRecordingsForArtist(artistMBID string) ([]LBTopRecording, error) { + return e.lb.TopRecordingsForArtist(e.ctx, artistMBID) +} + +// SimilarArtists returns artists similar to the given artist MBID. +func (e *Service) SimilarArtists(artistMBID string) ([]LBSimilarArtist, error) { + return e.lb.SimilarArtists(e.ctx, artistMBID) +} + +// --------------------------------------------------------------------------- +// Cover Art Archive +// --------------------------------------------------------------------------- + +// CoverArtURL returns the Cover Art Archive URL for a release's +// front cover at the default 250px size. +func (e *Service) CoverArtURL(releaseMBID string) string { + return CoverArtURL(releaseMBID) +} diff --git a/frontend/index.ts b/frontend/index.ts index a42ab71..3143e0d 100644 --- a/frontend/index.ts +++ b/frontend/index.ts @@ -16,6 +16,7 @@ import '@components/smart-playlist-editor/smart-playlist-editor.ts'; import '@components/search-bar/search-bar.ts'; import '@components/library-filter/library-filter.ts'; import '@components/track-details/track-details.ts'; +import '@components/explore-view/explore-view.ts'; import '@awesome.me/webawesome/dist/styles/themes/default.css'; import '@awesome.me/webawesome/dist/components/icon/icon.js'; import { setBasePath } from '@awesome.me/webawesome/dist/webawesome.js'; @@ -54,6 +55,7 @@ const VIEW_TAGS: Record = { artists: 'artists-view', genres: 'genres-view', playlists: 'playlist-view', + explore: 'explore-view', settings: 'config-page', }; diff --git a/frontend/src/components/explore-view/explore-view.ts b/frontend/src/components/explore-view/explore-view.ts new file mode 100644 index 0000000..bc7e30f --- /dev/null +++ b/frontend/src/components/explore-view/explore-view.ts @@ -0,0 +1,42 @@ +import { LitElement, html, css } from 'lit'; +import { customElement } from 'lit/decorators.js'; +import { designTokens } from '../../styles/tokens.css'; + +@customElement('explore-view') +export class ExploreView extends LitElement { + static override styles = [ + designTokens, + css` + :host { + display: block; + padding: 24px; + } + + h1 { + margin: 0 0 16px; + font-size: 1.5rem; + color: var(--yj-text-primary); + } + + .placeholder { + color: var(--yj-text-secondary); + } + `, + ]; + + override render() { + return html` +

Explore

+

+ Search MusicBrainz to discover artists, albums, and + tracks. +

+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'explore-view': ExploreView; + } +} diff --git a/frontend/src/components/sidebar/app-sidebar.ts b/frontend/src/components/sidebar/app-sidebar.ts index ed70950..44c5174 100644 --- a/frontend/src/components/sidebar/app-sidebar.ts +++ b/frontend/src/components/sidebar/app-sidebar.ts @@ -5,7 +5,7 @@ import { designTokens } from '../../styles/tokens.css'; import type { DragActiveDetail } from '@utils/drag-controller'; -type View = 'home' | 'playlists' | 'artists' | 'genres' | 'albums' | 'tracks' | 'settings'; +type View = 'home' | 'playlists' | 'artists' | 'genres' | 'albums' | 'tracks' | 'explore' | 'settings'; interface NavItem { id: View; @@ -148,6 +148,7 @@ export class AppSidebar extends LitElement { { id: 'genres', label: 'Genres', icon: 'masks-theater' }, { id: 'albums', label: 'Albums', icon: 'compact-disc' }, { id: 'tracks', label: 'Tracks', icon: 'music' }, + { id: 'explore', label: 'Explore', icon: 'globe' }, { id: 'settings', label: 'Settings', icon: 'gear' }, ];