perf(library): resolve album and genre file paths in one query

"Play this artist" awaited `GetAlbumTracks` inside a for loop — 13
sequential round trips for a 12-album artist — and every one of the
four sites doing that asked for whole track rows to read `FilePath`
off them. Five genres cost 6 MB across the IPC.

`GetFilePathsByAlbums(ids, libraryID)` and `GetFilePathsByGenres(names,
libraryID)` answer once and carry only the paths. Measured at 50 000
tracks: an artist 13 calls / 74.2 kB -> 2 / 19.2 kB, twenty albums
20 / 117.5 kB / 7.8 ms -> 1 / 26.0 kB / 1.7 ms, five genres
5 / 6 014 kB / 213 ms -> 1 / 1 291 kB / 32.6 ms, with the returned path
lists identical.

They return the paths grouped by album id or genre name rather than
flattened, because the caller owns the order — an album list is sorted
by name, not by id, and a flattened result would silently reorder a
queue — and because the album drag cache stores them per album. A
libraryID of 0 means "every library", matching an unset filter.
This commit is contained in:
2026-08-12 01:18:17 -04:00
parent 0cf710cf47
commit 9e0e4d5bb8
8 changed files with 625 additions and 0 deletions
+121
View File
@@ -1083,3 +1083,124 @@ func (l *Library) GetAllLibrariesWithTrackCounts() ([]Info, error) {
return result, nil
}
// GetFilePathsByAlbums returns the file paths of every track in the
// given albums, grouped by album id.
//
// "Play this artist", "play these albums" and the album drag cache each
// resolved paths with one binding call per album, sequentially, and each
// asked for whole track rows to read one field off them (perf.m2). This
// is that question asked once. The result is grouped rather than
// flattened because the caller owns the ordering — an album list is
// sorted by name, not by id — and because the drag cache stores it per
// album.
//
// A library id of 0 means "every library", matching the caller's
// selected-library filter being unset.
func (l *Library) GetFilePathsByAlbums(
albumIDs []int64, libraryID int64,
) (map[int64][]string, error) {
paths := make(map[int64][]string, len(albumIDs))
if len(albumIDs) == 0 {
return paths, nil
}
if libraryID > 0 {
rows, err := l.db.ReadQueries.GetFilePathsByReleaseGroupsByLibrary(
l.ctx, sqlcgen.GetFilePathsByReleaseGroupsByLibraryParams{
ReleaseGroupIds: albumIDs,
LibraryID: libraryID,
},
)
if err != nil {
l.logger.Error(
"could not retrieve album file paths for library",
"albums", len(albumIDs),
"libraryID", libraryID,
"error", err,
)
return nil, fmt.Errorf("could not get album file paths: %w", err)
}
for _, row := range rows {
paths[row.ReleaseGroupID] = append(paths[row.ReleaseGroupID], row.FilePath)
}
return paths, nil
}
rows, err := l.db.ReadQueries.GetFilePathsByReleaseGroups(l.ctx, albumIDs)
if err != nil {
l.logger.Error(
"could not retrieve album file paths",
"albums", len(albumIDs),
"error", err,
)
return nil, fmt.Errorf("could not get album file paths: %w", err)
}
for _, row := range rows {
paths[row.ReleaseGroupID] = append(paths[row.ReleaseGroupID], row.FilePath)
}
return paths, nil
}
// GetFilePathsByGenres returns the file paths of every track tagged with
// the given genres, grouped by genre name. See GetFilePathsByAlbums —
// same finding, same shape, and the caller still owns the de-duplication
// across genres because it owns the order.
func (l *Library) GetFilePathsByGenres(
genreNames []string, libraryID int64,
) (map[string][]string, error) {
paths := make(map[string][]string, len(genreNames))
if len(genreNames) == 0 {
return paths, nil
}
if libraryID > 0 {
rows, err := l.db.ReadQueries.GetFilePathsByGenresByLibrary(
l.ctx, sqlcgen.GetFilePathsByGenresByLibraryParams{
GenreNames: genreNames,
LibraryID: libraryID,
},
)
if err != nil {
l.logger.Error(
"could not retrieve genre file paths for library",
"genres", len(genreNames),
"libraryID", libraryID,
"error", err,
)
return nil, fmt.Errorf("could not get genre file paths: %w", err)
}
for _, row := range rows {
paths[row.GenreName] = append(paths[row.GenreName], row.FilePath)
}
return paths, nil
}
rows, err := l.db.ReadQueries.GetFilePathsByGenres(l.ctx, genreNames)
if err != nil {
l.logger.Error(
"could not retrieve genre file paths",
"genres", len(genreNames),
"error", err,
)
return nil, fmt.Errorf("could not get genre file paths: %w", err)
}
for _, row := range rows {
paths[row.GenreName] = append(paths[row.GenreName], row.FilePath)
}
return paths, nil
}