feat: MBIDs in library models + local-first search + explore cache
Backend: - Added mbid column to sqlc schemas for artists and release_groups - Regenerated sqlc queries to SELECT mbid in artist/album queries - Added MBID field to library.Artist and library.Album Go structs - All GetAllArtists/GetAllAlbums variants now populate MBID Frontend: - Updated Wails models.ts with MBID fields on Artist and Album - Added cachedArtists/cachedAlbums getters to LibraryStore - searchLibraryCache now includes MBIDs and local cover art URLs so library results can navigate to explore detail pages - Added mergeWithLibrary() — when full MB results arrive, library entries are enriched with local images and 'In Library' flags rather than being replaced by MB-only versions - Created ExploreCache store for cross-page data sharing: search results populate the cache, detail pages can read from it to avoid redundant API calls for already-fetched data
This commit is contained in:
@@ -32,7 +32,7 @@ SELECT * FROM artists
|
|||||||
ORDER BY name;
|
ORDER BY name;
|
||||||
|
|
||||||
-- name: GetAlbumArtists :many
|
-- name: GetAlbumArtists :many
|
||||||
SELECT DISTINCT a.id, a.name
|
SELECT DISTINCT a.id, a.name, a.mbid
|
||||||
FROM artists a
|
FROM artists a
|
||||||
JOIN artist_credit_artist aca ON aca.artist_id = a.id
|
JOIN artist_credit_artist aca ON aca.artist_id = a.id
|
||||||
JOIN artist_credit ac ON ac.id = aca.credit_id
|
JOIN artist_credit ac ON ac.id = aca.credit_id
|
||||||
@@ -40,7 +40,7 @@ JOIN release_groups rg ON rg.album_artist_credit_id = ac.id
|
|||||||
ORDER BY a.name;
|
ORDER BY a.name;
|
||||||
|
|
||||||
-- name: GetAlbumArtistsByLibrary :many
|
-- name: GetAlbumArtistsByLibrary :many
|
||||||
SELECT DISTINCT a.id, a.name
|
SELECT DISTINCT a.id, a.name, a.mbid
|
||||||
FROM artists a
|
FROM artists a
|
||||||
JOIN artist_credit_artist aca ON aca.artist_id = a.id
|
JOIN artist_credit_artist aca ON aca.artist_id = a.id
|
||||||
JOIN artist_credit ac ON ac.id = aca.credit_id
|
JOIN artist_credit ac ON ac.id = aca.credit_id
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ SELECT
|
|||||||
rg.id,
|
rg.id,
|
||||||
rg.name,
|
rg.name,
|
||||||
rg.year,
|
rg.year,
|
||||||
|
rg.mbid,
|
||||||
COALESCE(ac.text, fallback_ac.text, '') as artist_name,
|
COALESCE(ac.text, fallback_ac.text, '') as artist_name,
|
||||||
COALESCE(ca.file_path, '') as cover_art_path
|
COALESCE(ca.file_path, '') as cover_art_path
|
||||||
FROM release_groups rg
|
FROM release_groups rg
|
||||||
@@ -69,6 +70,7 @@ SELECT
|
|||||||
rg.id,
|
rg.id,
|
||||||
rg.name,
|
rg.name,
|
||||||
rg.year,
|
rg.year,
|
||||||
|
rg.mbid,
|
||||||
COALESCE(ac.text, fallback_ac.text, '') as artist_name,
|
COALESCE(ac.text, fallback_ac.text, '') as artist_name,
|
||||||
COALESCE(ca.file_path, '') as cover_art_path
|
COALESCE(ca.file_path, '') as cover_art_path
|
||||||
FROM release_groups rg
|
FROM release_groups rg
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
CREATE TABLE IF NOT EXISTS artists (
|
CREATE TABLE IF NOT EXISTS artists (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
name TEXT NOT NULL UNIQUE
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
mbid TEXT
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ CREATE TABLE IF NOT EXISTS release_groups (
|
|||||||
year INTEGER,
|
year INTEGER,
|
||||||
total_tracks INTEGER,
|
total_tracks INTEGER,
|
||||||
total_discs INTEGER,
|
total_discs INTEGER,
|
||||||
|
mbid TEXT,
|
||||||
FOREIGN KEY(cover_art_id) REFERENCES cover_art(id),
|
FOREIGN KEY(cover_art_id) REFERENCES cover_art(id),
|
||||||
FOREIGN KEY(album_artist_credit_id) REFERENCES artist_credit(id),
|
FOREIGN KEY(album_artist_credit_id) REFERENCES artist_credit(id),
|
||||||
UNIQUE(name, album_artist_credit_id)
|
UNIQUE(name, album_artist_credit_id)
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ import (
|
|||||||
|
|
||||||
const createArtist = `-- name: CreateArtist :one
|
const createArtist = `-- name: CreateArtist :one
|
||||||
INSERT INTO artists (name) VALUES (?)
|
INSERT INTO artists (name) VALUES (?)
|
||||||
RETURNING id, name
|
RETURNING id, name, mbid
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) CreateArtist(ctx context.Context, name string) (Artist, error) {
|
func (q *Queries) CreateArtist(ctx context.Context, name string) (Artist, error) {
|
||||||
row := q.db.QueryRowContext(ctx, createArtist, name)
|
row := q.db.QueryRowContext(ctx, createArtist, name)
|
||||||
var i Artist
|
var i Artist
|
||||||
err := row.Scan(&i.ID, &i.Name)
|
err := row.Scan(&i.ID, &i.Name, &i.Mbid)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ func (q *Queries) DeleteArtist(ctx context.Context, id int64) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getAlbumArtists = `-- name: GetAlbumArtists :many
|
const getAlbumArtists = `-- name: GetAlbumArtists :many
|
||||||
SELECT DISTINCT a.id, a.name
|
SELECT DISTINCT a.id, a.name, a.mbid
|
||||||
FROM artists a
|
FROM artists a
|
||||||
JOIN artist_credit_artist aca ON aca.artist_id = a.id
|
JOIN artist_credit_artist aca ON aca.artist_id = a.id
|
||||||
JOIN artist_credit ac ON ac.id = aca.credit_id
|
JOIN artist_credit ac ON ac.id = aca.credit_id
|
||||||
@@ -58,7 +58,7 @@ func (q *Queries) GetAlbumArtists(ctx context.Context) ([]Artist, error) {
|
|||||||
var items []Artist
|
var items []Artist
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var i Artist
|
var i Artist
|
||||||
if err := rows.Scan(&i.ID, &i.Name); err != nil {
|
if err := rows.Scan(&i.ID, &i.Name, &i.Mbid); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
items = append(items, i)
|
items = append(items, i)
|
||||||
@@ -73,7 +73,7 @@ func (q *Queries) GetAlbumArtists(ctx context.Context) ([]Artist, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getAlbumArtistsByLibrary = `-- name: GetAlbumArtistsByLibrary :many
|
const getAlbumArtistsByLibrary = `-- name: GetAlbumArtistsByLibrary :many
|
||||||
SELECT DISTINCT a.id, a.name
|
SELECT DISTINCT a.id, a.name, a.mbid
|
||||||
FROM artists a
|
FROM artists a
|
||||||
JOIN artist_credit_artist aca ON aca.artist_id = a.id
|
JOIN artist_credit_artist aca ON aca.artist_id = a.id
|
||||||
JOIN artist_credit ac ON ac.id = aca.credit_id
|
JOIN artist_credit ac ON ac.id = aca.credit_id
|
||||||
@@ -100,7 +100,7 @@ func (q *Queries) GetAlbumArtistsByLibrary(ctx context.Context, libraryID int64)
|
|||||||
var items []Artist
|
var items []Artist
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var i Artist
|
var i Artist
|
||||||
if err := rows.Scan(&i.ID, &i.Name); err != nil {
|
if err := rows.Scan(&i.ID, &i.Name, &i.Mbid); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
items = append(items, i)
|
items = append(items, i)
|
||||||
@@ -115,7 +115,7 @@ func (q *Queries) GetAlbumArtistsByLibrary(ctx context.Context, libraryID int64)
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getAllArtists = `-- name: GetAllArtists :many
|
const getAllArtists = `-- name: GetAllArtists :many
|
||||||
SELECT id, name FROM artists
|
SELECT id, name, mbid FROM artists
|
||||||
ORDER BY name
|
ORDER BY name
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -128,7 +128,7 @@ func (q *Queries) GetAllArtists(ctx context.Context) ([]Artist, error) {
|
|||||||
var items []Artist
|
var items []Artist
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var i Artist
|
var i Artist
|
||||||
if err := rows.Scan(&i.ID, &i.Name); err != nil {
|
if err := rows.Scan(&i.ID, &i.Name, &i.Mbid); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
items = append(items, i)
|
items = append(items, i)
|
||||||
@@ -143,26 +143,26 @@ func (q *Queries) GetAllArtists(ctx context.Context) ([]Artist, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getArtist = `-- name: GetArtist :one
|
const getArtist = `-- name: GetArtist :one
|
||||||
SELECT id, name FROM artists
|
SELECT id, name, mbid FROM artists
|
||||||
WHERE id = ? LIMIT 1
|
WHERE id = ? LIMIT 1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetArtist(ctx context.Context, id int64) (Artist, error) {
|
func (q *Queries) GetArtist(ctx context.Context, id int64) (Artist, error) {
|
||||||
row := q.db.QueryRowContext(ctx, getArtist, id)
|
row := q.db.QueryRowContext(ctx, getArtist, id)
|
||||||
var i Artist
|
var i Artist
|
||||||
err := row.Scan(&i.ID, &i.Name)
|
err := row.Scan(&i.ID, &i.Name, &i.Mbid)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const getArtistByName = `-- name: GetArtistByName :one
|
const getArtistByName = `-- name: GetArtistByName :one
|
||||||
SELECT id, name FROM artists
|
SELECT id, name, mbid FROM artists
|
||||||
WHERE name = ? LIMIT 1
|
WHERE name = ? LIMIT 1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetArtistByName(ctx context.Context, name string) (Artist, error) {
|
func (q *Queries) GetArtistByName(ctx context.Context, name string) (Artist, error) {
|
||||||
row := q.db.QueryRowContext(ctx, getArtistByName, name)
|
row := q.db.QueryRowContext(ctx, getArtistByName, name)
|
||||||
var i Artist
|
var i Artist
|
||||||
err := row.Scan(&i.ID, &i.Name)
|
err := row.Scan(&i.ID, &i.Name, &i.Mbid)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,12 +185,12 @@ func (q *Queries) UpdateArtist(ctx context.Context, arg UpdateArtistParams) erro
|
|||||||
const upsertArtist = `-- name: UpsertArtist :one
|
const upsertArtist = `-- name: UpsertArtist :one
|
||||||
INSERT INTO artists (name) VALUES (?)
|
INSERT INTO artists (name) VALUES (?)
|
||||||
ON CONFLICT(name) DO UPDATE SET name = excluded.name
|
ON CONFLICT(name) DO UPDATE SET name = excluded.name
|
||||||
RETURNING id, name
|
RETURNING id, name, mbid
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) UpsertArtist(ctx context.Context, name string) (Artist, error) {
|
func (q *Queries) UpsertArtist(ctx context.Context, name string) (Artist, error) {
|
||||||
row := q.db.QueryRowContext(ctx, upsertArtist, name)
|
row := q.db.QueryRowContext(ctx, upsertArtist, name)
|
||||||
var i Artist
|
var i Artist
|
||||||
err := row.Scan(&i.ID, &i.Name)
|
err := row.Scan(&i.ID, &i.Name, &i.Mbid)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
type Artist struct {
|
type Artist struct {
|
||||||
ID int64
|
ID int64
|
||||||
Name string
|
Name string
|
||||||
|
Mbid sql.NullString
|
||||||
}
|
}
|
||||||
|
|
||||||
type ArtistCredit struct {
|
type ArtistCredit struct {
|
||||||
@@ -154,6 +155,7 @@ type ReleaseGroup struct {
|
|||||||
Year sql.NullInt64
|
Year sql.NullInt64
|
||||||
TotalTracks sql.NullInt64
|
TotalTracks sql.NullInt64
|
||||||
TotalDiscs sql.NullInt64
|
TotalDiscs sql.NullInt64
|
||||||
|
Mbid sql.NullString
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReleaseGroupRecording struct {
|
type ReleaseGroupRecording struct {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func (q *Queries) CountReleaseGroupRecordings(ctx context.Context, releaseGroupI
|
|||||||
|
|
||||||
const createReleaseGroup = `-- name: CreateReleaseGroup :one
|
const createReleaseGroup = `-- name: CreateReleaseGroup :one
|
||||||
INSERT INTO release_groups (name) VALUES (?)
|
INSERT INTO release_groups (name) VALUES (?)
|
||||||
RETURNING id, name, cover_art_id, album_artist_credit_id, year, total_tracks, total_discs
|
RETURNING id, name, cover_art_id, album_artist_credit_id, year, total_tracks, total_discs, mbid
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) CreateReleaseGroup(ctx context.Context, name string) (ReleaseGroup, error) {
|
func (q *Queries) CreateReleaseGroup(ctx context.Context, name string) (ReleaseGroup, error) {
|
||||||
@@ -37,6 +37,7 @@ func (q *Queries) CreateReleaseGroup(ctx context.Context, name string) (ReleaseG
|
|||||||
&i.Year,
|
&i.Year,
|
||||||
&i.TotalTracks,
|
&i.TotalTracks,
|
||||||
&i.TotalDiscs,
|
&i.TotalDiscs,
|
||||||
|
&i.Mbid,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@@ -45,7 +46,7 @@ const createReleaseGroupFull = `-- name: CreateReleaseGroupFull :one
|
|||||||
INSERT INTO release_groups (
|
INSERT INTO release_groups (
|
||||||
name, cover_art_id, album_artist_credit_id, year, total_tracks, total_discs
|
name, cover_art_id, album_artist_credit_id, year, total_tracks, total_discs
|
||||||
) VALUES (?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?)
|
||||||
RETURNING id, name, cover_art_id, album_artist_credit_id, year, total_tracks, total_discs
|
RETURNING id, name, cover_art_id, album_artist_credit_id, year, total_tracks, total_discs, mbid
|
||||||
`
|
`
|
||||||
|
|
||||||
type CreateReleaseGroupFullParams struct {
|
type CreateReleaseGroupFullParams struct {
|
||||||
@@ -75,6 +76,7 @@ func (q *Queries) CreateReleaseGroupFull(ctx context.Context, arg CreateReleaseG
|
|||||||
&i.Year,
|
&i.Year,
|
||||||
&i.TotalTracks,
|
&i.TotalTracks,
|
||||||
&i.TotalDiscs,
|
&i.TotalDiscs,
|
||||||
|
&i.Mbid,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@@ -233,6 +235,7 @@ SELECT
|
|||||||
rg.id,
|
rg.id,
|
||||||
rg.name,
|
rg.name,
|
||||||
rg.year,
|
rg.year,
|
||||||
|
rg.mbid,
|
||||||
COALESCE(ac.text, fallback_ac.text, '') as artist_name,
|
COALESCE(ac.text, fallback_ac.text, '') as artist_name,
|
||||||
COALESCE(ca.file_path, '') as cover_art_path
|
COALESCE(ca.file_path, '') as cover_art_path
|
||||||
FROM release_groups rg
|
FROM release_groups rg
|
||||||
@@ -252,6 +255,7 @@ type GetAllAlbumsWithDetailsRow struct {
|
|||||||
ID int64
|
ID int64
|
||||||
Name string
|
Name string
|
||||||
Year sql.NullInt64
|
Year sql.NullInt64
|
||||||
|
Mbid sql.NullString
|
||||||
ArtistName string
|
ArtistName string
|
||||||
CoverArtPath string
|
CoverArtPath string
|
||||||
}
|
}
|
||||||
@@ -269,6 +273,7 @@ func (q *Queries) GetAllAlbumsWithDetails(ctx context.Context) ([]GetAllAlbumsWi
|
|||||||
&i.ID,
|
&i.ID,
|
||||||
&i.Name,
|
&i.Name,
|
||||||
&i.Year,
|
&i.Year,
|
||||||
|
&i.Mbid,
|
||||||
&i.ArtistName,
|
&i.ArtistName,
|
||||||
&i.CoverArtPath,
|
&i.CoverArtPath,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
@@ -290,6 +295,7 @@ SELECT
|
|||||||
rg.id,
|
rg.id,
|
||||||
rg.name,
|
rg.name,
|
||||||
rg.year,
|
rg.year,
|
||||||
|
rg.mbid,
|
||||||
COALESCE(ac.text, fallback_ac.text, '') as artist_name,
|
COALESCE(ac.text, fallback_ac.text, '') as artist_name,
|
||||||
COALESCE(ca.file_path, '') as cover_art_path
|
COALESCE(ca.file_path, '') as cover_art_path
|
||||||
FROM release_groups rg
|
FROM release_groups rg
|
||||||
@@ -316,6 +322,7 @@ type GetAllAlbumsWithDetailsByLibraryRow struct {
|
|||||||
ID int64
|
ID int64
|
||||||
Name string
|
Name string
|
||||||
Year sql.NullInt64
|
Year sql.NullInt64
|
||||||
|
Mbid sql.NullString
|
||||||
ArtistName string
|
ArtistName string
|
||||||
CoverArtPath string
|
CoverArtPath string
|
||||||
}
|
}
|
||||||
@@ -333,6 +340,7 @@ func (q *Queries) GetAllAlbumsWithDetailsByLibrary(ctx context.Context, libraryI
|
|||||||
&i.ID,
|
&i.ID,
|
||||||
&i.Name,
|
&i.Name,
|
||||||
&i.Year,
|
&i.Year,
|
||||||
|
&i.Mbid,
|
||||||
&i.ArtistName,
|
&i.ArtistName,
|
||||||
&i.CoverArtPath,
|
&i.CoverArtPath,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
@@ -350,7 +358,7 @@ func (q *Queries) GetAllAlbumsWithDetailsByLibrary(ctx context.Context, libraryI
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getAllReleaseGroups = `-- name: GetAllReleaseGroups :many
|
const getAllReleaseGroups = `-- name: GetAllReleaseGroups :many
|
||||||
SELECT id, name, cover_art_id, album_artist_credit_id, year, total_tracks, total_discs FROM release_groups
|
SELECT id, name, cover_art_id, album_artist_credit_id, year, total_tracks, total_discs, mbid FROM release_groups
|
||||||
ORDER BY name
|
ORDER BY name
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -371,6 +379,7 @@ func (q *Queries) GetAllReleaseGroups(ctx context.Context) ([]ReleaseGroup, erro
|
|||||||
&i.Year,
|
&i.Year,
|
||||||
&i.TotalTracks,
|
&i.TotalTracks,
|
||||||
&i.TotalDiscs,
|
&i.TotalDiscs,
|
||||||
|
&i.Mbid,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -386,7 +395,7 @@ func (q *Queries) GetAllReleaseGroups(ctx context.Context) ([]ReleaseGroup, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getReleaseGroup = `-- name: GetReleaseGroup :one
|
const getReleaseGroup = `-- name: GetReleaseGroup :one
|
||||||
SELECT id, name, cover_art_id, album_artist_credit_id, year, total_tracks, total_discs FROM release_groups
|
SELECT id, name, cover_art_id, album_artist_credit_id, year, total_tracks, total_discs, mbid FROM release_groups
|
||||||
WHERE id = ? LIMIT 1
|
WHERE id = ? LIMIT 1
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -401,12 +410,13 @@ func (q *Queries) GetReleaseGroup(ctx context.Context, id int64) (ReleaseGroup,
|
|||||||
&i.Year,
|
&i.Year,
|
||||||
&i.TotalTracks,
|
&i.TotalTracks,
|
||||||
&i.TotalDiscs,
|
&i.TotalDiscs,
|
||||||
|
&i.Mbid,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const getReleaseGroupByNameAndArtist = `-- name: GetReleaseGroupByNameAndArtist :one
|
const getReleaseGroupByNameAndArtist = `-- name: GetReleaseGroupByNameAndArtist :one
|
||||||
SELECT id, name, cover_art_id, album_artist_credit_id, year, total_tracks, total_discs FROM release_groups
|
SELECT id, name, cover_art_id, album_artist_credit_id, year, total_tracks, total_discs, mbid FROM release_groups
|
||||||
WHERE name = ? AND album_artist_credit_id = ? LIMIT 1
|
WHERE name = ? AND album_artist_credit_id = ? LIMIT 1
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -426,6 +436,7 @@ func (q *Queries) GetReleaseGroupByNameAndArtist(ctx context.Context, arg GetRel
|
|||||||
&i.Year,
|
&i.Year,
|
||||||
&i.TotalTracks,
|
&i.TotalTracks,
|
||||||
&i.TotalDiscs,
|
&i.TotalDiscs,
|
||||||
|
&i.Mbid,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@@ -468,7 +479,7 @@ VALUES (?, ?, ?)
|
|||||||
ON CONFLICT(name, album_artist_credit_id) DO UPDATE SET
|
ON CONFLICT(name, album_artist_credit_id) DO UPDATE SET
|
||||||
album_artist_credit_id = COALESCE(excluded.album_artist_credit_id, release_groups.album_artist_credit_id),
|
album_artist_credit_id = COALESCE(excluded.album_artist_credit_id, release_groups.album_artist_credit_id),
|
||||||
year = COALESCE(excluded.year, release_groups.year)
|
year = COALESCE(excluded.year, release_groups.year)
|
||||||
RETURNING id, name, cover_art_id, album_artist_credit_id, year, total_tracks, total_discs
|
RETURNING id, name, cover_art_id, album_artist_credit_id, year, total_tracks, total_discs, mbid
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpsertReleaseGroupParams struct {
|
type UpsertReleaseGroupParams struct {
|
||||||
@@ -488,6 +499,7 @@ func (q *Queries) UpsertReleaseGroup(ctx context.Context, arg UpsertReleaseGroup
|
|||||||
&i.Year,
|
&i.Year,
|
||||||
&i.TotalTracks,
|
&i.TotalTracks,
|
||||||
&i.TotalDiscs,
|
&i.TotalDiscs,
|
||||||
|
&i.Mbid,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ func (l *Library) GetTrackMBIDs(filePath string) TrackMBIDs {
|
|||||||
type Artist struct {
|
type Artist struct {
|
||||||
ID int64
|
ID int64
|
||||||
Name string
|
Name string
|
||||||
|
MBID string
|
||||||
ImageSmall string
|
ImageSmall string
|
||||||
ImageMedium string
|
ImageMedium string
|
||||||
ImageLarge string
|
ImageLarge string
|
||||||
@@ -157,6 +158,7 @@ type Album struct {
|
|||||||
ID int64
|
ID int64
|
||||||
Name string
|
Name string
|
||||||
ArtistName string
|
ArtistName string
|
||||||
|
MBID string
|
||||||
CoverArtPath string
|
CoverArtPath string
|
||||||
CoverArtSmall string
|
CoverArtSmall string
|
||||||
CoverArtMedium string
|
CoverArtMedium string
|
||||||
@@ -331,6 +333,10 @@ func (l *Library) GetAllAlbums() ([]Album, error) {
|
|||||||
album.Year = row.Year.Int64
|
album.Year = row.Year.Int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if row.Mbid.Valid {
|
||||||
|
album.MBID = row.Mbid.String
|
||||||
|
}
|
||||||
|
|
||||||
// Convert filesystem path to URL path for the asset handler.
|
// Convert filesystem path to URL path for the asset handler.
|
||||||
if row.CoverArtPath != "" {
|
if row.CoverArtPath != "" {
|
||||||
urls := coverart.ResolveURLs(row.CoverArtPath)
|
urls := coverart.ResolveURLs(row.CoverArtPath)
|
||||||
@@ -366,10 +372,16 @@ func (l *Library) GetAllArtists() ([]Artist, error) {
|
|||||||
artists := make([]Artist, 0, len(rows))
|
artists := make([]Artist, 0, len(rows))
|
||||||
|
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
artists = append(artists, Artist{
|
a := Artist{
|
||||||
ID: row.ID,
|
ID: row.ID,
|
||||||
Name: row.Name,
|
Name: row.Name,
|
||||||
})
|
}
|
||||||
|
|
||||||
|
if row.Mbid.Valid {
|
||||||
|
a.MBID = row.Mbid.String
|
||||||
|
}
|
||||||
|
|
||||||
|
artists = append(artists, a)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve artist image URLs from the disk cache.
|
// Resolve artist image URLs from the disk cache.
|
||||||
@@ -663,6 +675,10 @@ func (l *Library) GetAllAlbumsByLibrary(
|
|||||||
album.Year = row.Year.Int64
|
album.Year = row.Year.Int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if row.Mbid.Valid {
|
||||||
|
album.MBID = row.Mbid.String
|
||||||
|
}
|
||||||
|
|
||||||
if row.CoverArtPath != "" {
|
if row.CoverArtPath != "" {
|
||||||
urls := coverart.ResolveURLs(row.CoverArtPath)
|
urls := coverart.ResolveURLs(row.CoverArtPath)
|
||||||
album.CoverArtPath = urls.Original
|
album.CoverArtPath = urls.Original
|
||||||
@@ -706,10 +722,16 @@ func (l *Library) GetAllArtistsByLibrary(
|
|||||||
artists := make([]Artist, 0, len(rows))
|
artists := make([]Artist, 0, len(rows))
|
||||||
|
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
artists = append(artists, Artist{
|
a := Artist{
|
||||||
ID: row.ID,
|
ID: row.ID,
|
||||||
Name: row.Name,
|
Name: row.Name,
|
||||||
})
|
}
|
||||||
|
|
||||||
|
if row.Mbid.Valid {
|
||||||
|
a.MBID = row.Mbid.String
|
||||||
|
}
|
||||||
|
|
||||||
|
artists = append(artists, a)
|
||||||
}
|
}
|
||||||
|
|
||||||
l.resolveArtistImages(artists)
|
l.resolveArtistImages(artists)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import type {
|
|||||||
MBRecording,
|
MBRecording,
|
||||||
} from '@go/explore/Service';
|
} from '@go/explore/Service';
|
||||||
import { libraryStore } from '../../store/library-store';
|
import { libraryStore } from '../../store/library-store';
|
||||||
|
import { exploreCache } from '../../store/explore-cache';
|
||||||
import '@awesome.me/webawesome/dist/components/icon/icon.js';
|
import '@awesome.me/webawesome/dist/components/icon/icon.js';
|
||||||
|
|
||||||
/* ── Constants ── */
|
/* ── Constants ── */
|
||||||
@@ -525,6 +526,10 @@ export class ExploreView extends LitElement {
|
|||||||
const localResults = this.searchLibraryCache(query);
|
const localResults = this.searchLibraryCache(query);
|
||||||
if (localResults && (localResults.artists?.length || localResults.releaseGroups?.length)) {
|
if (localResults && (localResults.artists?.length || localResults.releaseGroups?.length)) {
|
||||||
this.results = localResults;
|
this.results = localResults;
|
||||||
|
exploreCache.populateFromSearch(
|
||||||
|
localResults.artists || [],
|
||||||
|
localResults.releaseGroups || [],
|
||||||
|
);
|
||||||
this.loadThumbnails();
|
this.loadThumbnails();
|
||||||
this.loadArtistImages();
|
this.loadArtistImages();
|
||||||
const elapsed = (performance.now() - startTime).toFixed(0);
|
const elapsed = (performance.now() - startTime).toFixed(0);
|
||||||
@@ -541,7 +546,8 @@ export class ExploreView extends LitElement {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Search the frontend library cache for matching artists and albums.
|
* Search the frontend library cache for matching artists and albums.
|
||||||
* Pure JS — no Go calls, guaranteed instant.
|
* Pure JS — no Go calls, guaranteed instant. Returns results with
|
||||||
|
* MBIDs and local cover art so they can navigate to explore pages.
|
||||||
*/
|
*/
|
||||||
private searchLibraryCache(query: string): MBSearchResult | null {
|
private searchLibraryCache(query: string): MBSearchResult | null {
|
||||||
const q = query.toLowerCase();
|
const q = query.toLowerCase();
|
||||||
@@ -552,14 +558,17 @@ export class ExploreView extends LitElement {
|
|||||||
for (const a of cachedArtists) {
|
for (const a of cachedArtists) {
|
||||||
if (a.Name.toLowerCase().includes(q)) {
|
if (a.Name.toLowerCase().includes(q)) {
|
||||||
artists.push({
|
artists.push({
|
||||||
mbid: '',
|
mbid: a.MBID || '',
|
||||||
name: a.Name,
|
name: a.Name,
|
||||||
sortName: '',
|
sortName: '',
|
||||||
type: 'Group',
|
type: '',
|
||||||
country: '',
|
country: '',
|
||||||
disambiguation: '',
|
disambiguation: '',
|
||||||
score: 100,
|
score: 100,
|
||||||
} as MBArtist);
|
_imageSmall: a.ImageSmall || '',
|
||||||
|
_imageMedium: a.ImageMedium || '',
|
||||||
|
_inLibrary: true,
|
||||||
|
} as MBArtist & { _imageSmall: string; _imageMedium: string; _inLibrary: boolean });
|
||||||
if (artists.length >= 5) break;
|
if (artists.length >= 5) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -571,12 +580,14 @@ export class ExploreView extends LitElement {
|
|||||||
for (const a of cachedAlbums) {
|
for (const a of cachedAlbums) {
|
||||||
if (a.Name.toLowerCase().includes(q) || a.ArtistName.toLowerCase().includes(q)) {
|
if (a.Name.toLowerCase().includes(q) || a.ArtistName.toLowerCase().includes(q)) {
|
||||||
releaseGroups.push({
|
releaseGroups.push({
|
||||||
mbid: '',
|
mbid: a.MBID || '',
|
||||||
title: a.Name,
|
title: a.Name,
|
||||||
primaryType: 'Album',
|
primaryType: 'Album',
|
||||||
artistCredit: a.ArtistName,
|
artistCredit: a.ArtistName,
|
||||||
firstReleaseDate: a.Year ? String(a.Year) : '',
|
firstReleaseDate: a.Year ? String(a.Year) : '',
|
||||||
} as MBReleaseGroup);
|
_coverArt: a.CoverArtMedium || a.CoverArtSmall || '',
|
||||||
|
_inLibrary: true,
|
||||||
|
} as MBReleaseGroup & { _coverArt: string; _inLibrary: boolean });
|
||||||
if (releaseGroups.length >= 5) break;
|
if (releaseGroups.length >= 5) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -589,6 +600,61 @@ export class ExploreView extends LitElement {
|
|||||||
return { artists, releaseGroups, recordings: [] } as MBSearchResult;
|
return { artists, releaseGroups, recordings: [] } as MBSearchResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge full search results with library data: library entries
|
||||||
|
* take priority (local art, "In Library" badge). MB-only results
|
||||||
|
* are appended after library matches.
|
||||||
|
*/
|
||||||
|
private mergeWithLibrary(result: MBSearchResult): MBSearchResult {
|
||||||
|
const cachedArtists = libraryStore.cachedArtists;
|
||||||
|
const cachedAlbums = libraryStore.cachedAlbums;
|
||||||
|
|
||||||
|
// Build MBID→library lookups.
|
||||||
|
const libArtistsByMBID = new Map<string, typeof cachedArtists extends (infer T)[] | null ? T : never>();
|
||||||
|
const libArtistsByName = new Map<string, typeof cachedArtists extends (infer T)[] | null ? T : never>();
|
||||||
|
if (cachedArtists) {
|
||||||
|
for (const a of cachedArtists) {
|
||||||
|
if (a.MBID) libArtistsByMBID.set(a.MBID, a);
|
||||||
|
libArtistsByName.set(a.Name.toLowerCase(), a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const libAlbumsByMBID = new Map<string, typeof cachedAlbums extends (infer T)[] | null ? T : never>();
|
||||||
|
if (cachedAlbums) {
|
||||||
|
for (const a of cachedAlbums) {
|
||||||
|
if (a.MBID) libAlbumsByMBID.set(a.MBID, a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enrich artists: if MB result matches a library artist, add local images.
|
||||||
|
if (result.artists) {
|
||||||
|
for (let i = 0; i < result.artists.length; i++) {
|
||||||
|
const a = result.artists[i];
|
||||||
|
const lib = (a.mbid && libArtistsByMBID.get(a.mbid)) ||
|
||||||
|
libArtistsByName.get(a.name.toLowerCase());
|
||||||
|
if (lib) {
|
||||||
|
(a as any)._imageSmall = lib.ImageSmall || '';
|
||||||
|
(a as any)._imageMedium = lib.ImageMedium || '';
|
||||||
|
(a as any)._inLibrary = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enrich release groups: if MB result matches a library album, use local art.
|
||||||
|
if (result.releaseGroups) {
|
||||||
|
for (let i = 0; i < result.releaseGroups.length; i++) {
|
||||||
|
const rg = result.releaseGroups[i];
|
||||||
|
const lib = rg.mbid ? libAlbumsByMBID.get(rg.mbid) : undefined;
|
||||||
|
if (lib) {
|
||||||
|
(rg as any)._coverArt = lib.CoverArtMedium || lib.CoverArtSmall || '';
|
||||||
|
(rg as any)._inLibrary = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
private async executeFullSearch(version: number, query: string, startTime: number) {
|
private async executeFullSearch(version: number, query: string, startTime: number) {
|
||||||
try {
|
try {
|
||||||
const result = await Search(query);
|
const result = await Search(query);
|
||||||
@@ -601,7 +667,11 @@ export class ExploreView extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.results = result;
|
this.results = this.mergeWithLibrary(result);
|
||||||
|
exploreCache.populateFromSearch(
|
||||||
|
this.results.artists || [],
|
||||||
|
this.results.releaseGroups || [],
|
||||||
|
);
|
||||||
this.loadThumbnails();
|
this.loadThumbnails();
|
||||||
this.loadArtistImages();
|
this.loadArtistImages();
|
||||||
this.checkLibrary();
|
this.checkLibrary();
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
/**
|
||||||
|
* ExploreCache — a simple in-memory cache for explore data that
|
||||||
|
* persists across page navigations within a session. Populated by
|
||||||
|
* search results and consumed by detail pages to avoid redundant
|
||||||
|
* API calls.
|
||||||
|
*
|
||||||
|
* Data flows:
|
||||||
|
* search results → cache artist images, album art, release groups
|
||||||
|
* artist detail page → check cache before API calls
|
||||||
|
* album detail page → check cache before API calls
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { MBReleaseGroup, LBTopRecording } from '@go/explore/Service';
|
||||||
|
|
||||||
|
/** Cached artist data from search results. */
|
||||||
|
export interface CachedArtist {
|
||||||
|
mbid: string;
|
||||||
|
name: string;
|
||||||
|
imageURL?: string; // resolved artist image
|
||||||
|
imageSmall?: string; // library small image
|
||||||
|
imageMedium?: string; // library medium image
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cached album data from search results. */
|
||||||
|
export interface CachedAlbum {
|
||||||
|
mbid: string;
|
||||||
|
title: string;
|
||||||
|
artistName: string;
|
||||||
|
coverArt?: string; // local cover art URL
|
||||||
|
year?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExploreCacheStore {
|
||||||
|
private artists = new Map<string, CachedArtist>();
|
||||||
|
private albums = new Map<string, CachedAlbum>();
|
||||||
|
private artistAlbums = new Map<string, MBReleaseGroup[]>();
|
||||||
|
private artistTopTracks = new Map<string, LBTopRecording[]>();
|
||||||
|
|
||||||
|
// -- Artists --
|
||||||
|
|
||||||
|
setArtist(mbid: string, data: CachedArtist) {
|
||||||
|
if (mbid) this.artists.set(mbid, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
getArtist(mbid: string): CachedArtist | undefined {
|
||||||
|
return this.artists.get(mbid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Albums --
|
||||||
|
|
||||||
|
setAlbum(mbid: string, data: CachedAlbum) {
|
||||||
|
if (mbid) this.albums.set(mbid, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAlbum(mbid: string): CachedAlbum | undefined {
|
||||||
|
return this.albums.get(mbid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Artist → Albums (release groups) --
|
||||||
|
|
||||||
|
setArtistAlbums(artistMBID: string, albums: MBReleaseGroup[]) {
|
||||||
|
if (artistMBID) this.artistAlbums.set(artistMBID, albums);
|
||||||
|
}
|
||||||
|
|
||||||
|
getArtistAlbums(artistMBID: string): MBReleaseGroup[] | undefined {
|
||||||
|
return this.artistAlbums.get(artistMBID);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Artist → Top tracks --
|
||||||
|
|
||||||
|
setArtistTopTracks(artistMBID: string, tracks: LBTopRecording[]) {
|
||||||
|
if (artistMBID) this.artistTopTracks.set(artistMBID, tracks);
|
||||||
|
}
|
||||||
|
|
||||||
|
getArtistTopTracks(artistMBID: string): LBTopRecording[] | undefined {
|
||||||
|
return this.artistTopTracks.get(artistMBID);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Bulk populate from search results --
|
||||||
|
|
||||||
|
populateFromSearch(artists: any[], releaseGroups: any[]) {
|
||||||
|
for (const a of artists) {
|
||||||
|
if (a.mbid) {
|
||||||
|
this.setArtist(a.mbid, {
|
||||||
|
mbid: a.mbid,
|
||||||
|
name: a.name,
|
||||||
|
imageSmall: a._imageSmall,
|
||||||
|
imageMedium: a._imageMedium,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const rg of releaseGroups) {
|
||||||
|
if (rg.mbid) {
|
||||||
|
this.setAlbum(rg.mbid, {
|
||||||
|
mbid: rg.mbid,
|
||||||
|
title: rg.title,
|
||||||
|
artistName: rg.artistCredit || '',
|
||||||
|
coverArt: rg._coverArt,
|
||||||
|
year: rg.firstReleaseDate,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const exploreCache = new ExploreCacheStore();
|
||||||
-3
@@ -1,7 +1,6 @@
|
|||||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
// This file is automatically generated. DO NOT EDIT
|
// This file is automatically generated. DO NOT EDIT
|
||||||
import {explore} from '../models';
|
import {explore} from '../models';
|
||||||
import {http} from '../models';
|
|
||||||
import {context} from '../models';
|
import {context} from '../models';
|
||||||
|
|
||||||
export function BrowseReleaseGroups(arg1:string):Promise<Array<explore.MBReleaseGroup>>;
|
export function BrowseReleaseGroups(arg1:string):Promise<Array<explore.MBReleaseGroup>>;
|
||||||
@@ -38,8 +37,6 @@ export function SearchArtists(arg1:string):Promise<Array<explore.MBArtist>>;
|
|||||||
|
|
||||||
export function SearchLocal(arg1:string):Promise<explore.MBSearchResult>;
|
export function SearchLocal(arg1:string):Promise<explore.MBSearchResult>;
|
||||||
|
|
||||||
export function SearchLocalHandler():Promise<http.Handler>;
|
|
||||||
|
|
||||||
export function SearchRecordings(arg1:string):Promise<Array<explore.MBRecording>>;
|
export function SearchRecordings(arg1:string):Promise<Array<explore.MBRecording>>;
|
||||||
|
|
||||||
export function SearchReleaseGroups(arg1:string):Promise<Array<explore.MBReleaseGroup>>;
|
export function SearchReleaseGroups(arg1:string):Promise<Array<explore.MBReleaseGroup>>;
|
||||||
|
|||||||
@@ -70,10 +70,6 @@ export function SearchLocal(arg1) {
|
|||||||
return window['go']['explore']['Service']['SearchLocal'](arg1);
|
return window['go']['explore']['Service']['SearchLocal'](arg1);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SearchLocalHandler() {
|
|
||||||
return window['go']['explore']['Service']['SearchLocalHandler']();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SearchRecordings(arg1) {
|
export function SearchRecordings(arg1) {
|
||||||
return window['go']['explore']['Service']['SearchRecordings'](arg1);
|
return window['go']['explore']['Service']['SearchRecordings'](arg1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -242,6 +242,7 @@ export namespace library {
|
|||||||
ID: number;
|
ID: number;
|
||||||
Name: string;
|
Name: string;
|
||||||
ArtistName: string;
|
ArtistName: string;
|
||||||
|
MBID: string;
|
||||||
CoverArtPath: string;
|
CoverArtPath: string;
|
||||||
CoverArtSmall: string;
|
CoverArtSmall: string;
|
||||||
CoverArtMedium: string;
|
CoverArtMedium: string;
|
||||||
@@ -257,6 +258,7 @@ export namespace library {
|
|||||||
this.ID = source["ID"];
|
this.ID = source["ID"];
|
||||||
this.Name = source["Name"];
|
this.Name = source["Name"];
|
||||||
this.ArtistName = source["ArtistName"];
|
this.ArtistName = source["ArtistName"];
|
||||||
|
this.MBID = source["MBID"];
|
||||||
this.CoverArtPath = source["CoverArtPath"];
|
this.CoverArtPath = source["CoverArtPath"];
|
||||||
this.CoverArtSmall = source["CoverArtSmall"];
|
this.CoverArtSmall = source["CoverArtSmall"];
|
||||||
this.CoverArtMedium = source["CoverArtMedium"];
|
this.CoverArtMedium = source["CoverArtMedium"];
|
||||||
@@ -267,6 +269,7 @@ export namespace library {
|
|||||||
export class Artist {
|
export class Artist {
|
||||||
ID: number;
|
ID: number;
|
||||||
Name: string;
|
Name: string;
|
||||||
|
MBID: string;
|
||||||
ImageSmall: string;
|
ImageSmall: string;
|
||||||
ImageMedium: string;
|
ImageMedium: string;
|
||||||
ImageLarge: string;
|
ImageLarge: string;
|
||||||
@@ -279,6 +282,7 @@ export namespace library {
|
|||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.ID = source["ID"];
|
this.ID = source["ID"];
|
||||||
this.Name = source["Name"];
|
this.Name = source["Name"];
|
||||||
|
this.MBID = source["MBID"];
|
||||||
this.ImageSmall = source["ImageSmall"];
|
this.ImageSmall = source["ImageSmall"];
|
||||||
this.ImageMedium = source["ImageMedium"];
|
this.ImageMedium = source["ImageMedium"];
|
||||||
this.ImageLarge = source["ImageLarge"];
|
this.ImageLarge = source["ImageLarge"];
|
||||||
|
|||||||
Reference in New Issue
Block a user