chore(M004/S01): auto-commit after complete-slice
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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' },
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user