music: respect individual artist names [#66]

Modify the music loader to use the normal artist name when using song
titles while still retaining album artist functionality.

Oftentimes music files will be tagged as to use the artist tag to
specify performers, collaborators, and others, and then use the album
artist tag to group them up into a single artist. Previously, Auxio
would only use the album artist tag, which flattened the collaborator
information out for consistency. Resolve this by implementing a sort
of "resolved artist name" for songs that is used in the UI and nowhere
else. This seems to work well, but at the same time further ruins the
API surface for handling music objects. An acceptable price to pay
for a better UX.
This commit is contained in:
OxygenCobalt 2022-01-30 16:31:29 -07:00
parent 50f6f8f348
commit 50a2305f63
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
23 changed files with 74 additions and 87 deletions

View file

@ -198,8 +198,12 @@ class ArtistDetailAdapter(
binding.detailName.text = data.resolvedName
binding.detailSubhead.text = data.genre?.resolvedName
?: context.getString(R.string.def_genre)
// Get the genre that corresponds to the most songs in this artist, which would be
// the most "Prominent" genre.
binding.detailSubhead.text = data.songs
.groupBy { it.genre?.resolvedName }
.entries.maxByOrNull { it.value.size }
?.key ?: context.getString(R.string.def_genre)
binding.detailInfo.text = context.getString(
R.string.fmt_counts,

View file

@ -60,6 +60,8 @@ class SongListFragment : HomeListFragment() {
val song = homeModel.songs.value!![idx]
// Change how we display the popup depending on the mode.
// We don't use the more correct resolve(Model)Name here, as sorts are largely
// based off the names of the parent objects and not the child objects.
when (homeModel.getSortForDisplay(DisplayMode.SHOW_SONGS)) {
// Name -> Use name
is Sort.ByName -> song.name.sliceArticle()

View file

@ -54,16 +54,6 @@ sealed class MusicParent : Music() {
/**
* The data object for a song. Inherits [BaseModel].
* @property fileName The raw filename for this track
* @property albumId The Song's Album ID.
* Never use this outside of when attaching a song to its album.
* @property track The Song's Track number
* @property duration The duration of the song, in millis.
* @property album The Song's parent album. Use this instead of [albumId].
* @property genre The Song's [Genre].
* These are not ensured to be linked due to possible quirks in the genre loading system.
* @property seconds The Song's duration in seconds
* @property formattedDuration The Song's duration as a duration string.
*/
data class Song(
override val id: Long,
@ -71,7 +61,8 @@ data class Song(
val fileName: String,
val albumName: String,
val albumId: Long,
val artistName: String,
val artistName: String?,
val albumArtistName: String?,
val year: Int,
val track: Int,
val duration: Long
@ -94,6 +85,14 @@ data class Song(
return result
}
/** An album name resolved to this song in particular. */
val resolvedAlbumName: String get() =
album.resolvedName
/** An artist name resolved to this song in particular. */
val resolvedArtistName: String get() =
artistName ?: album.artist.resolvedName
fun linkAlbum(album: Album) {
mAlbum = album
}
@ -105,11 +104,6 @@ data class Song(
/**
* The data object for an album. Inherits [MusicParent].
* @property artistName The name of the parent artist. Do not use this outside of creating the artist from albums
* @property year The year this album was released. 0 if there is none in the metadata.
* @property artist The Album's parent [Artist]. use this instead of [artistName]
* @property songs The Album's child [Song]s.
* @property totalDuration The combined duration of all of the album's child songs, formatted.
*/
data class Album(
override val id: Long,
@ -130,9 +124,11 @@ data class Album(
val totalDuration: String get() =
songs.sumOf { it.seconds }.toDuration(false)
fun linkArtist(artist: Artist) {
mArtist = artist
}
override val resolvedName: String
get() = name
val resolvedArtistName: String get() =
artist.resolvedName
override val hash: Long get() {
var result = name.hashCode().toLong()
@ -141,14 +137,15 @@ data class Album(
return result
}
override val resolvedName: String
get() = name
fun linkArtist(artist: Artist) {
mArtist = artist
}
}
/**
* The data object for an artist. Inherits [MusicParent]
* The data object for an *album* artist. Inherits [MusicParent]. This differs from the actual
* performers.
* @property albums The list of all [Album]s in this artist
* @property genre The most prominent genre for this artist
* @property songs The list of all [Song]s in this artist
*/
data class Artist(
@ -163,16 +160,7 @@ data class Artist(
}
}
val genre: Genre? by lazy {
// Get the genre that corresponds to the most songs in this artist, which would be
// the most "Prominent" genre.
songs.groupBy { it.genre }.entries.maxByOrNull { it.value.size }?.key
}
val songs: List<Song> by lazy {
albums.flatMap { it.songs }
}
val songs = albums.flatMap { it.songs }
override val hash = name.hashCode().toLong()
}

View file

@ -142,12 +142,10 @@ class MusicLoader {
val album = cursor.getString(albumIndex)
val albumId = cursor.getLong(albumIdIndex)
// MediaStore does not have support for artists in the album field, so we have to
// detect it on a song-by-song basis. This is another massive bottleneck in the music
// loader since we have to do a massive query to get what we want, but theres not
// a lot I can do that doesn't degrade UX.
val artist = cursor.getStringOrNull(albumArtistIndex)
?: cursor.getString(artistIndex)
val artist = cursor.getString(artistIndex).let {
if (it != MediaStore.UNKNOWN_STRING) it else null
}
val albumArtist = cursor.getStringOrNull(albumArtistIndex)
val year = cursor.getInt(yearIndex)
val track = cursor.getInt(trackIndex)
@ -155,7 +153,8 @@ class MusicLoader {
songs.add(
Song(
id, title, fileName, album, albumId, artist, year, track, duration
id, title, fileName, album, albumId, artist,
albumArtist, year, track, duration
)
)
}
@ -181,11 +180,11 @@ class MusicLoader {
albums.add(
Album(
id = entry.key,
name = song.albumName,
artistName = song.artistName,
songs = entry.value,
year = song.year
// When assigning an artist to an album, use the album artist first, then the
// normal artist, and then the internal representation of an unknown artist name.
entry.key, song.albumName,
song.albumArtistName ?: song.artistName ?: MediaStore.UNKNOWN_STRING,
song.year, entry.value
)
)
}
@ -210,10 +209,8 @@ class MusicLoader {
// Use the hashCode of the artist name as our ID and move on.
artists.add(
Artist(
id = entry.key.hashCode().toLong(),
name = entry.key,
resolvedName = resolvedName,
albums = entry.value
entry.key.hashCode().toLong(), entry.key,
resolvedName, entry.value
)
)
}
@ -243,10 +240,12 @@ class MusicLoader {
// No non-broken genre would be missing a name.
val id = cursor.getLong(idIndex)
val name = cursor.getStringOrNull(nameIndex) ?: continue
val resolvedName = when (name) {
MediaStore.UNKNOWN_STRING -> context.getString(R.string.def_genre)
else -> name.getGenreNameCompat() ?: name
}
val genre = Genre(
id, name, name.getGenreNameCompat() ?: name
)
val genre = Genre(id, name, resolvedName)
linkGenre(context, genre, songs)
genres.add(genre)

View file

@ -79,12 +79,12 @@ class PlaybackNotification private constructor(
*/
fun setMetadata(song: Song, onDone: () -> Unit) {
setContentTitle(song.name)
setContentText(song.album.artist.resolvedName)
setContentText(song.resolvedArtistName)
// On older versions of android [API <24], show the song's album on the subtext instead of
// the current mode, as that makes more sense for the old style of media notifications.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
setSubText(song.album.name)
setSubText(song.resolvedAlbumName)
}
// loadBitmap() is concurrent, so only call back to the object calling this function when

View file

@ -114,7 +114,7 @@ class PlaybackSessionConnector(
return
}
val artistName = song.album.artist.resolvedName
val artistName = song.resolvedArtistName
val builder = MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, song.name)
@ -123,7 +123,7 @@ class PlaybackSessionConnector(
.putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, artistName)
.putString(MediaMetadataCompat.METADATA_KEY_COMPOSER, artistName)
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, artistName)
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, song.album.name)
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, song.resolvedAlbumName)
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.duration)
// Load the cover asynchronously. This is the entire reason I don't use a plain

View file

@ -102,7 +102,7 @@ private fun RemoteViews.applyMeta(context: Context, state: WidgetState): RemoteV
applyCover(context, state)
setTextViewText(R.id.widget_song, state.song.name)
setTextViewText(R.id.widget_artist, state.song.album.artist.resolvedName)
setTextViewText(R.id.widget_artist, state.song.resolvedArtistName)
return this
}
@ -111,7 +111,8 @@ private fun RemoteViews.applyCover(context: Context, state: WidgetState): Remote
if (state.albumArt != null) {
setImageViewBitmap(R.id.widget_cover, state.albumArt)
setContentDescription(
R.id.widget_cover, context.getString(R.string.desc_album_cover, state.song.album.name)
R.id.widget_cover,
context.getString(R.string.desc_album_cover, state.song.resolvedAlbumName)
)
} else {
setImageViewResource(R.id.widget_cover, R.drawable.ic_widget_album)

View file

@ -82,7 +82,7 @@
android:layout_marginStart="@dimen/spacing_mid_large"
android:layout_marginEnd="@dimen/spacing_mid_large"
android:onClick="@{() -> detailModel.navToItem(playbackModel.song.album.artist)}"
android:text="@{song.album.artist.resolvedName}"
android:text="@{song.resolvedArtistName}"
app:layout_constraintBottom_toTopOf="@+id/playback_album"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
@ -98,7 +98,7 @@
android:layout_marginStart="@dimen/spacing_mid_large"
android:layout_marginEnd="@dimen/spacing_mid_large"
android:onClick="@{() -> detailModel.navToItem(playbackModel.song.album)}"
android:text="@{song.album.name}"
android:text="@{song.resolvedAlbumName}"
app:layout_constraintBottom_toBottomOf="@+id/playback_cover"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"

View file

@ -79,7 +79,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:onClick="@{() -> detailModel.navToItem(playbackModel.song.album.artist)}"
android:text="@{song.album.artist.resolvedName}"
android:text="@{song.resolvedArtistName}"
app:layout_constraintBottom_toTopOf="@+id/playback_album"
app:layout_constraintStart_toStartOf="@+id/playback_song_container"
app:layout_constraintEnd_toEndOf="@+id/playback_song_container"
@ -93,7 +93,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:onClick="@{() -> detailModel.navToItem(playbackModel.song.album)}"
android:text="@{song.album.name}"
android:text="@{song.resolvedAlbumName}"
app:layout_constraintBottom_toTopOf="@+id/playback_seek_bar"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="@+id/playback_song_container"

View file

@ -70,7 +70,7 @@
android:layout_marginStart="@dimen/spacing_large"
android:layout_marginEnd="@dimen/spacing_large"
android:onClick="@{() -> detailModel.navToItem(playbackModel.song.album.artist)}"
android:text="@{song.album.artist.resolvedName}"
android:text="@{song.resolvedArtistName}"
app:layout_constraintBottom_toTopOf="@+id/playback_album"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
@ -84,7 +84,7 @@
android:layout_marginStart="@dimen/spacing_large"
android:layout_marginEnd="@dimen/spacing_large"
android:onClick="@{() -> detailModel.navToItem(playbackModel.song.album)}"
android:text="@{song.album.name}"
android:text="@{song.resolvedAlbumName}"
app:layout_constraintBottom_toTopOf="@+id/playback_seek_bar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"

View file

@ -52,7 +52,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_small"
android:ellipsize="end"
android:text="@{@string/fmt_two(song.album.artist.resolvedName, song.album.name)}"
android:text="@{@string/fmt_two(song.resolvedArtistName, song.resolvedAlbumName)}"
app:layout_constraintBottom_toBottomOf="@+id/playback_cover"
app:layout_constraintEnd_toEndOf="@+id/playback_song"
app:layout_constraintStart_toEndOf="@+id/playback_cover"

View file

@ -80,7 +80,7 @@
android:layout_marginStart="@dimen/spacing_mid_large"
android:layout_marginEnd="@dimen/spacing_mid_large"
android:onClick="@{() -> detailModel.navToItem(playbackModel.song.album.artist)}"
android:text="@{song.album.artist.resolvedName}"
android:text="@{song.resolvedArtistName}"
app:layout_constraintBottom_toTopOf="@+id/playback_album"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
@ -96,7 +96,7 @@
android:layout_marginStart="@dimen/spacing_mid_large"
android:layout_marginEnd="@dimen/spacing_mid_large"
android:onClick="@{() -> detailModel.navToItem(playbackModel.song.album)}"
android:text="@{song.album.name}"
android:text="@{song.resolvedAlbumName}"
app:layout_constraintBottom_toTopOf="@+id/playback_seek_bar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"

View file

@ -50,7 +50,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_small"
android:ellipsize="end"
android:text="@{@string/fmt_two(song.album.artist.resolvedName, song.album.name)}"
android:text="@{@string/fmt_two(song.resolvedArtistName, song.resolvedAlbumName)}"
app:layout_constraintBottom_toBottomOf="@+id/playback_cover"
app:layout_constraintEnd_toEndOf="@+id/playback_song"
app:layout_constraintStart_toEndOf="@+id/playback_cover"

View file

@ -69,7 +69,7 @@
android:layout_marginStart="@dimen/spacing_mid_large"
android:layout_marginEnd="@dimen/spacing_mid_large"
android:onClick="@{() -> detailModel.navToItem(playbackModel.song.album.artist)}"
android:text="@{song.album.artist.resolvedName}"
android:text="@{song.resolvedArtistName}"
app:layout_constraintBottom_toTopOf="@+id/playback_album"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
@ -83,7 +83,7 @@
android:layout_marginStart="@dimen/spacing_mid_large"
android:layout_marginEnd="@dimen/spacing_mid_large"
android:onClick="@{() -> detailModel.navToItem(playbackModel.song.album)}"
android:text="@{song.album.name}"
android:text="@{song.resolvedAlbumName}"
app:layout_constraintBottom_toTopOf="@+id/playback_seek_bar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"

View file

@ -41,7 +41,7 @@
style="@style/Widget.Auxio.TextView.Item.Secondary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{@string/fmt_two(album.artist.resolvedName, @plurals/fmt_song_count(album.songs.size, album.songs.size))}"
android:text="@{@string/fmt_two(album.resolvedArtistName, @plurals/fmt_song_count(album.songs.size, album.songs.size))}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/album_cover"

View file

@ -25,7 +25,6 @@
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.Auxio.TitleMedium"
android:textSize="20sp"
android:fontFamily="sans-serif"
android:textColor="@color/sel_accented_secondary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"

View file

@ -44,7 +44,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/spacing_medium"
android:text="@{song.album.name}"
android:text="@{song.resolvedAlbumName}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/song_duration"
app:layout_constraintStart_toEndOf="@+id/album_cover"

View file

@ -44,7 +44,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/spacing_medium"
android:text="@{@string/fmt_two(song.album.artist.resolvedName, song.album.name)}"
android:text="@{@string/fmt_two(song.resolvedArtistName, song.resolvedAlbumName)}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/song_duration"
app:layout_constraintStart_toEndOf="@+id/album_cover"

View file

@ -69,7 +69,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/spacing_medium"
android:text="@{@string/fmt_two(song.album.artist.resolvedName, song.album.name)}"
android:text="@{@string/fmt_two(song.resolvedArtistName, song.resolvedAlbumName)}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/song_drag_handle"
app:layout_constraintStart_toEndOf="@+id/album_cover"

View file

@ -42,7 +42,7 @@
style="@style/Widget.Auxio.TextView.Item.Secondary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{@string/fmt_two(song.album.artist.resolvedName, song.album.name)}"
android:text="@{@string/fmt_two(song.resolvedArtistName, song.resolvedAlbumName)}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/album_cover"

View file

@ -51,7 +51,7 @@
android:layout_marginStart="@dimen/spacing_small"
android:layout_marginEnd="@dimen/spacing_small"
android:ellipsize="end"
android:text="@{@string/fmt_two(song.album.artist.resolvedName, song.album.name)}"
android:text="@{@string/fmt_two(song.resolvedArtistName, song.resolvedAlbumName)}"
app:layout_constraintBottom_toBottomOf="@+id/playback_cover"
app:layout_constraintEnd_toStartOf="@+id/playback_play_pause"
app:layout_constraintStart_toEndOf="@+id/playback_cover"

View file

@ -25,6 +25,7 @@
android:padding="@dimen/spacing_medium"
android:text="@string/def_playback"
android:textAppearance="@style/TextAppearance.Auxio.TitleMidLarge"
android:fontFamily="sans-serif-medium"
android:textColor="?android:attr/textColorPrimary" />
</FrameLayout>

View file

@ -89,14 +89,7 @@
<item name="fontFamily">@font/inter_semibold</item>
<item name="android:fontFamily">@font/inter_semibold</item>
<item name="android:textStyle">normal</item>
<item name="android:textSize">20sp</item>
</style>
<style name="TextAppearance.Auxio.TitleSmallish" parent="TextAppearance.Material3.TitleSmall">
<item name="fontFamily">@font/inter_semibold</item>
<item name="android:fontFamily">@font/inter_semibold</item>
<item name="android:textStyle">normal</item>
<item name="android:textSize">16sp</item>
<item name="android:textSize">18sp</item>
</style>
<style name="TextAppearance.Auxio.LabelLarger" parent="TextAppearance.Auxio.LabelLarge">