mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-21 13:09:46 +02:00
proper alignment of filters
This commit is contained in:
+6
@@ -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(
|
||||||
|
|||||||
+9
-2
@@ -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.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
|
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",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,12 +400,15 @@ 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' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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({
|
items.push({
|
||||||
key: 'section-songs',
|
key: 'section-songs',
|
||||||
type: 'section',
|
type: 'section',
|
||||||
@@ -407,6 +419,7 @@ function buildListItems(
|
|||||||
detail.songTracks.slice(0, SONG_PREVIEW_LIMIT).forEach((track, index) => {
|
detail.songTracks.slice(0, SONG_PREVIEW_LIMIT).forEach((track, index) => {
|
||||||
items.push({ key: `song-${track.id}`, type: 'track', track, section: 'songs', index });
|
items.push({ key: `song-${track.id}`, type: 'track', track, section: 'songs', index });
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (detail.showAppearances) {
|
if (detail.showAppearances) {
|
||||||
items.push({
|
items.push({
|
||||||
|
|||||||
@@ -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[] {
|
||||||
|
|||||||
@@ -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' }),
|
||||||
|
|||||||
@@ -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.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);
|
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)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user