mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-21 21:16:09 +02:00
proper alignment of filters
This commit is contained in:
+6
@@ -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(
|
||||
|
||||
+10
-3
@@ -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
|
||||
aggregate.albumKeys += row.identityKey
|
||||
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",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,22 +400,26 @@ 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' });
|
||||
}
|
||||
|
||||
items.push({
|
||||
key: 'section-songs',
|
||||
type: 'section',
|
||||
title: 'Songs',
|
||||
trailing: formatCount(songCount, 'track'),
|
||||
target: 'songs',
|
||||
});
|
||||
detail.songTracks.slice(0, SONG_PREVIEW_LIMIT).forEach((track, index) => {
|
||||
items.push({ key: `song-${track.id}`, type: 'track', track, section: 'songs', index });
|
||||
});
|
||||
// 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',
|
||||
title: 'Songs',
|
||||
trailing: formatCount(songCount, 'track'),
|
||||
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) {
|
||||
items.push({
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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' }),
|
||||
|
||||
@@ -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;
|
||||
aggregate.albumKeys.add(track.album_identity_key);
|
||||
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)) {
|
||||
|
||||
Reference in New Issue
Block a user