proper alignment of filters

This commit is contained in:
Boof2015
2026-08-02 23:14:15 -04:00
parent 9153d91ccf
commit de27a4439d
6 changed files with 101 additions and 30 deletions
@@ -906,6 +906,10 @@ interface CatalogDao {
groupingMode: String, groupingMode: String,
): List<ArtistSummaryEntity> ): List<ArtistSummaryEntity>
// 'song' only: an album the artist merely guests on (a Various Artists
// compilation carrying one of their tracks) is not one of their albums. Those
// tracks are already reachable through the detail page's "Appears On" section,
// which reads the 'appearance' half of the same index.
@Query( @Query(
""" """
SELECT DISTINCT a.* SELECT DISTINCT a.*
@@ -916,6 +920,7 @@ interface CatalogDao {
AND i.revision = :revision AND i.revision = :revision
AND i.grouping_mode = :groupingMode AND i.grouping_mode = :groupingMode
AND i.artist_key = :artistKey AND i.artist_key = :artistKey
AND i.relationship = 'song'
ORDER BY a.latest_added_at DESC, a.name_sort_key, a.identity_key ORDER BY a.latest_added_at DESC, a.name_sort_key, a.identity_key
LIMIT :limit OFFSET :offset LIMIT :limit OFFSET :offset
""", """,
@@ -936,6 +941,7 @@ interface CatalogDao {
WHERE i.revision = :revision WHERE i.revision = :revision
AND i.grouping_mode = :groupingMode AND i.grouping_mode = :groupingMode
AND i.artist_key = :artistKey AND i.artist_key = :artistKey
AND i.relationship = 'song'
""", """,
) )
suspend fun countArtistAlbums( suspend fun countArtistAlbums(
@@ -260,13 +260,20 @@ object CatalogReadModelBuilder {
} else { } else {
canonicalArtistNames(track) canonicalArtistNames(track)
} }
val primaryKey = normalizeKey(primary)
for (name in names) { for (name in names) {
val key = normalizeKey(name) val key = normalizeKey(name)
if (key.isEmpty()) continue if (key.isEmpty()) continue
val isPrimary = key == primaryKey
val aggregate = aggregates.getOrPut(key) { Aggregate(name) } val aggregate = aggregates.getOrPut(key) { Aggregate(name) }
aggregate.trackCount += 1 aggregate.trackCount += 1
if (key == normalizeKey(primary)) aggregate.primaryCount += 1 if (isPrimary) {
aggregate.albumKeys += row.identityKey aggregate.primaryCount += 1
// Albums the artist only guests on are not their albums; counting
// them here credited a Various Artists compilation to every featured
// performer. Artwork stays ungated so those artists keep a mosaic.
aggregate.albumKeys += row.identityKey
}
val current = aggregate.artworkTrack val current = aggregate.artworkTrack
if (track.artworkHash != null && (current == null || newerArtwork(track, current))) { if (track.artworkHash != null && (current == null || newerArtwork(track, current))) {
aggregate.artworkTrack = track aggregate.artworkTrack = track
@@ -277,7 +284,7 @@ object CatalogReadModelBuilder {
groupingMode = mode, groupingMode = mode,
artistKey = key, artistKey = key,
trackId = track.id, trackId = track.id,
relationship = if (key == normalizeKey(primary)) "song" else "appearance", relationship = if (isPrimary) "song" else "appearance",
) )
} }
} }
+31 -18
View File
@@ -127,8 +127,14 @@ export default function ArtistScreen() {
]); ]);
const listItems = useMemo( const listItems = useMemo(
() => buildListItems(detail, songsPage.totalCount, appearancesPage.totalCount), () =>
[appearancesPage.totalCount, detail, songsPage.totalCount] buildListItems(
detail,
albumsPage.totalCount,
songsPage.totalCount,
appearancesPage.totalCount
),
[albumsPage.totalCount, appearancesPage.totalCount, detail, songsPage.totalCount]
); );
const playTrackListFrom = ( const playTrackListFrom = (
@@ -331,11 +337,13 @@ export default function ArtistScreen() {
title={name} title={name}
heroMeta={ heroMeta={
<View style={styles.stats}> <View style={styles.stats}>
{(allPage.summary?.album_count ?? detail.albums.length) > 0 ? ( {/*
<StatChip The album page's own count, not summary.album_count: the stored
icon="albums-outline" summary counts albums the artist only appears on, and only settles
label={formatCount(allPage.summary?.album_count ?? detail.albums.length, 'album')} on the next rescan.
/> */}
{albumsPage.totalCount > 0 ? (
<StatChip icon="albums-outline" label={formatCount(albumsPage.totalCount, 'album')} />
) : null} ) : null}
<StatChip icon="musical-notes-outline" label={formatCount(allPage.totalCount, 'track')} /> <StatChip icon="musical-notes-outline" label={formatCount(allPage.totalCount, 'track')} />
{detail.totalDuration > 0 ? ( {detail.totalDuration > 0 ? (
@@ -376,6 +384,7 @@ export default function ArtistScreen() {
function buildListItems( function buildListItems(
detail: ArtistDetail, detail: ArtistDetail,
albumCount: number,
songCount: number, songCount: number,
appearanceCount: number appearanceCount: number
): ArtistPageItem[] { ): ArtistPageItem[] {
@@ -391,22 +400,26 @@ function buildListItems(
key: 'section-albums', key: 'section-albums',
type: 'section', type: 'section',
title: 'Albums', title: 'Albums',
trailing: formatCount(detail.albums.length, 'album'), trailing: formatCount(albumCount, 'album'),
target: 'albums', target: 'albums',
}); });
items.push({ key: 'albums', type: 'albums' }); items.push({ key: 'albums', type: 'albums' });
} }
items.push({ // An artist who only guests on other people's albums has no songs of their
key: 'section-songs', // own, and a bare "Songs · 0 tracks" header reads as a loading failure.
type: 'section', if (songCount > 0) {
title: 'Songs', items.push({
trailing: formatCount(songCount, 'track'), key: 'section-songs',
target: 'songs', type: 'section',
}); title: 'Songs',
detail.songTracks.slice(0, SONG_PREVIEW_LIMIT).forEach((track, index) => { trailing: formatCount(songCount, 'track'),
items.push({ key: `song-${track.id}`, type: 'track', track, section: 'songs', index }); target: 'songs',
}); });
detail.songTracks.slice(0, SONG_PREVIEW_LIMIT).forEach((track, index) => {
items.push({ key: `song-${track.id}`, type: 'track', track, section: 'songs', index });
});
}
if (detail.showAppearances) { if (detail.showAppearances) {
items.push({ items.push({
+8 -7
View File
@@ -2,7 +2,6 @@ import {
filterTracksByArtist, filterTracksByArtist,
normalizeKey, normalizeKey,
resolveCanonicalBrowseArtist, resolveCanonicalBrowseArtist,
splitAlbumArtistCollaborators,
type ArtistGroupingMode, type ArtistGroupingMode,
} from '@/library/artistGrouping'; } from '@/library/artistGrouping';
import type { DbTrack } from '@/types/library'; import type { DbTrack } from '@/types/library';
@@ -60,14 +59,16 @@ export function buildArtistDetail(
}; };
} }
/**
* "Main" means the album is theirs, so the test is the canonical primary artist
* and nothing else — the same rule the native read model uses to mark an index
* row 'song' rather than 'appearance'. Matching on the raw track artist as well
* made a Various Artists compilation count as a main album for every featured
* performer on it.
*/
function isMainArtistTrack(track: DbTrack, artistKey: string): boolean { function isMainArtistTrack(track: DbTrack, artistKey: string): boolean {
if (!artistKey) return false; if (!artistKey) return false;
if (normalizeKey(resolveCanonicalBrowseArtist(track)) === artistKey) return true; return normalizeKey(resolveCanonicalBrowseArtist(track)) === artistKey;
if (normalizeKey(track.artist) === artistKey) return true;
if (normalizeKey(track.album_artist ?? '') === artistKey) return true;
return splitAlbumArtistCollaborators(track.album_artist ?? '').some(
(name) => normalizeKey(name) === artistKey
);
} }
function buildArtistAlbums(tracks: readonly DbTrack[]): ArtistAlbum[] { function buildArtistAlbums(tracks: readonly DbTrack[]): ArtistAlbum[] {
+39
View File
@@ -83,6 +83,45 @@ test('file-tags artist records count every indexed track as primary', () => {
assert.ok(artists.every((artist) => artist.primary_track_count === artist.track_count)); assert.ok(artists.every((artist) => artist.primary_track_count === artist.track_count));
}); });
test('a compilation counts as an album only for its album artist', () => {
const compilation = {
album_artist: 'Various Artists',
album_identity_key: 'album:comp',
artwork_hash: 'comp-art',
};
const artists = buildArtistList([
createTrack({ artist: 'Yoko Takahashi', ...compilation }),
createTrack({ artist: 'Megumi Hayashibara', ...compilation }),
createTrack({
artist: 'Yoko Takahashi',
album_artist: 'Yoko Takahashi',
album_identity_key: 'album:own',
artwork_hash: 'own-art',
}),
], 'astra');
const byName = new Map(artists.map((artist) => [artist.artist, artist]));
// Featured on the compilation, but it is not one of her albums — only the
// record she is the album artist of counts. Her track total still includes it.
const featured = byName.get('Yoko Takahashi');
assert.ok(featured);
assert.equal(featured.track_count, 2);
assert.equal(featured.album_count, 1);
// Nothing but the compilation: no albums, but the cover still feeds the mosaic.
const appearanceOnly = byName.get('Megumi Hayashibara');
assert.ok(appearanceOnly);
assert.equal(appearanceOnly.track_count, 1);
assert.equal(appearanceOnly.primary_track_count, 0);
assert.equal(appearanceOnly.album_count, 0);
assert.deepEqual(appearanceOnly.artwork_hashes, ['comp-art']);
const various = byName.get('Various Artists');
assert.ok(various);
assert.equal(various.album_count, 1);
});
test('artist browse filter defaults to primary artists and restores collab-only artists', () => { test('artist browse filter defaults to primary artists and restores collab-only artists', () => {
const artists = buildArtistList([ const artists = buildArtistList([
createTrack({ artist: 'Primary Artist feat. Guest Artist' }), createTrack({ artist: 'Primary Artist feat. Guest Artist' }),
+7 -2
View File
@@ -209,8 +209,13 @@ export function buildArtistList(tracks: readonly ArtistTrackLike[], mode: Artist
byKey.set(key, aggregate); byKey.set(key, aggregate);
} }
aggregate.track_count += 1; aggregate.track_count += 1;
if (key === primaryArtistKey) aggregate.primary_track_count += 1; if (key === primaryArtistKey) {
aggregate.albumKeys.add(track.album_identity_key); aggregate.primary_track_count += 1;
// Albums the artist only guests on are not their albums; counting them
// here credited a Various Artists compilation to every featured
// performer. albumArtwork below stays ungated so they keep a mosaic.
aggregate.albumKeys.add(track.album_identity_key);
}
if (!track.artwork_hash) continue; if (!track.artwork_hash) continue;
if (!aggregate.albumArtwork.has(track.album_identity_key)) { if (!aggregate.albumArtwork.has(track.album_identity_key)) {