chore(M004/S01): auto-commit after complete-slice

This commit is contained in:
2026-03-23 16:55:22 -04:00
parent 6cc9acbc99
commit 1ff53d8084
5 changed files with 173 additions and 1 deletions
+9
View File
@@ -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)
+118
View File
@@ -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)
}
+2
View File
@@ -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<string, string> = {
artists: 'artists-view',
genres: 'genres-view',
playlists: 'playlist-view',
explore: 'explore-view',
settings: 'config-page',
};
@@ -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`
<h1>Explore</h1>
<p class="placeholder">
Search MusicBrainz to discover artists, albums, and
tracks.
</p>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'explore-view': ExploreView;
}
}
@@ -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' },
];