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,
): 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(
"""
SELECT DISTINCT a.*
@@ -916,6 +920,7 @@ interface CatalogDao {
AND i.revision = :revision
AND i.grouping_mode = :groupingMode
AND i.artist_key = :artistKey
AND i.relationship = 'song'
ORDER BY a.latest_added_at DESC, a.name_sort_key, a.identity_key
LIMIT :limit OFFSET :offset
""",
@@ -936,6 +941,7 @@ interface CatalogDao {
WHERE i.revision = :revision
AND i.grouping_mode = :groupingMode
AND i.artist_key = :artistKey
AND i.relationship = 'song'
""",
)
suspend fun countArtistAlbums(
@@ -260,13 +260,20 @@ object CatalogReadModelBuilder {
} else {
canonicalArtistNames(track)
}
val primaryKey = normalizeKey(primary)
for (name in names) {
val key = normalizeKey(name)
if (key.isEmpty()) continue
val isPrimary = key == primaryKey
val aggregate = aggregates.getOrPut(key) { Aggregate(name) }
aggregate.trackCount += 1
if (key == normalizeKey(primary)) aggregate.primaryCount += 1
if (isPrimary) {
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
if (track.artworkHash != null && (current == null || newerArtwork(track, current))) {
aggregate.artworkTrack = track
@@ -277,7 +284,7 @@ object CatalogReadModelBuilder {
groupingMode = mode,
artistKey = key,
trackId = track.id,
relationship = if (key == normalizeKey(primary)) "song" else "appearance",
relationship = if (isPrimary) "song" else "appearance",
)
}
}
+21 -8
View File
@@ -127,8 +127,14 @@ export default function ArtistScreen() {
]);
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 = (
@@ -331,11 +337,13 @@ export default function ArtistScreen() {
title={name}
heroMeta={
<View style={styles.stats}>
{(allPage.summary?.album_count ?? detail.albums.length) > 0 ? (
<StatChip
icon="albums-outline"
label={formatCount(allPage.summary?.album_count ?? detail.albums.length, 'album')}
/>
{/*
The album page's own count, not summary.album_count: the stored
summary counts albums the artist only appears on, and only settles
on the next rescan.
*/}
{albumsPage.totalCount > 0 ? (
<StatChip icon="albums-outline" label={formatCount(albumsPage.totalCount, 'album')} />
) : null}
<StatChip icon="musical-notes-outline" label={formatCount(allPage.totalCount, 'track')} />
{detail.totalDuration > 0 ? (
@@ -376,6 +384,7 @@ export default function ArtistScreen() {
function buildListItems(
detail: ArtistDetail,
albumCount: number,
songCount: number,
appearanceCount: number
): ArtistPageItem[] {
@@ -391,12 +400,15 @@ function buildListItems(
key: 'section-albums',
type: 'section',
title: 'Albums',
trailing: formatCount(detail.albums.length, 'album'),
trailing: formatCount(albumCount, 'album'),
target: 'albums',
});
items.push({ key: 'albums', type: 'albums' });
}
// An artist who only guests on other people's albums has no songs of their
// own, and a bare "Songs · 0 tracks" header reads as a loading failure.
if (songCount > 0) {
items.push({
key: 'section-songs',
type: 'section',
@@ -407,6 +419,7 @@ function buildListItems(
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) {
items.push({
+8 -7
View File
@@ -2,7 +2,6 @@ import {
filterTracksByArtist,
normalizeKey,
resolveCanonicalBrowseArtist,
splitAlbumArtistCollaborators,
type ArtistGroupingMode,
} from '@/library/artistGrouping';
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 {
if (!artistKey) return false;
if (normalizeKey(resolveCanonicalBrowseArtist(track)) === artistKey) return true;
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
);
return normalizeKey(resolveCanonicalBrowseArtist(track)) === artistKey;
}
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));
});
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', () => {
const artists = buildArtistList([
createTrack({ artist: 'Primary Artist feat. Guest Artist' }),
+6 -1
View File
@@ -209,8 +209,13 @@ export function buildArtistList(tracks: readonly ArtistTrackLike[], mode: Artist
byKey.set(key, aggregate);
}
aggregate.track_count += 1;
if (key === primaryArtistKey) aggregate.primary_track_count += 1;
if (key === primaryArtistKey) {
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 (!aggregate.albumArtwork.has(track.album_identity_key)) {