detail: group implicit albums in "appears on"

Group albums implicitly linked to an artist via the "artist" tag into
their own section called "Appears on".

This makes Auxio's artist model a bit more apparent to users.

Resolves #411.
This commit is contained in:
Alexander Capehart 2023-05-25 12:54:23 -06:00
parent 8939d341e6
commit ba94d4fa21
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
7 changed files with 69 additions and 43 deletions

View file

@ -5,6 +5,15 @@
#### What's New #### What's New
- Added ability to share a track - Added ability to share a track
#### What's Improved
- Tracks with no disc number now default to "No Disc" instead of "Disc 1"
- Albums implicitly linked only via "artist" tags are now placed in a special
"appears on" section in the artist view
#### What's Fixed
- Prevented options such as "Add to queue" from being selected on empty
artists and playlists
## 3.1.0 ## 3.1.0
#### What's New #### What's New

View file

@ -428,32 +428,42 @@ constructor(
private fun refreshArtistList(artist: Artist, replace: Boolean = false) { private fun refreshArtistList(artist: Artist, replace: Boolean = false) {
logD("Refreshing artist list") logD("Refreshing artist list")
val list = mutableListOf<Item>() val list = mutableListOf<Item>()
val albums = Sort(Sort.Mode.ByDate, Sort.Direction.DESCENDING).albums(artist.albums)
val byReleaseGroup = val grouping =
albums.groupBy { Sort(Sort.Mode.ByDate, Sort.Direction.DESCENDING)
// Remap the complicated ReleaseType data structure into an easier .albums(artist.explicitAlbums)
// "AlbumGrouping" enum that will automatically group and sort .groupByTo(sortedMapOf()) {
// the artist's albums. // Remap the complicated ReleaseType data structure into an easier
when (it.releaseType.refinement) { // "AlbumGrouping" enum that will automatically group and sort
ReleaseType.Refinement.LIVE -> AlbumGrouping.LIVE // the artist's albums.
ReleaseType.Refinement.REMIX -> AlbumGrouping.REMIXES when (it.releaseType.refinement) {
null -> ReleaseType.Refinement.LIVE -> AlbumGrouping.LIVE
when (it.releaseType) { ReleaseType.Refinement.REMIX -> AlbumGrouping.REMIXES
is ReleaseType.Album -> AlbumGrouping.ALBUMS null ->
is ReleaseType.EP -> AlbumGrouping.EPS when (it.releaseType) {
is ReleaseType.Single -> AlbumGrouping.SINGLES is ReleaseType.Album -> AlbumGrouping.ALBUMS
is ReleaseType.Compilation -> AlbumGrouping.COMPILATIONS is ReleaseType.EP -> AlbumGrouping.EPS
is ReleaseType.Soundtrack -> AlbumGrouping.SOUNDTRACKS is ReleaseType.Single -> AlbumGrouping.SINGLES
is ReleaseType.Mix -> AlbumGrouping.MIXES is ReleaseType.Compilation -> AlbumGrouping.COMPILATIONS
is ReleaseType.Mixtape -> AlbumGrouping.MIXTAPES is ReleaseType.Soundtrack -> AlbumGrouping.SOUNDTRACKS
} is ReleaseType.Mix -> AlbumGrouping.DJMIXES
is ReleaseType.Mixtape -> AlbumGrouping.MIXTAPES
}
}
} }
}
logD("Release groups for this artist: ${byReleaseGroup.keys}") if (artist.implicitAlbums.isNotEmpty()) {
// groupByTo normally returns a mapping to a MutableList mapping. Since MutableList
// inherits list, we can cast upwards and save a copy by directly inserting the
// implicit album list into the mapping.
@Suppress("UNCHECKED_CAST")
(grouping as MutableMap<AlbumGrouping, List<Album>>)[AlbumGrouping.APPEARANCES] =
Sort(Sort.Mode.ByDate, Sort.Direction.DESCENDING).albums(artist.implicitAlbums)
}
for (entry in byReleaseGroup.entries.sortedBy { it.key }) { logD("Release groups for this artist: ${grouping.keys}")
for (entry in grouping.entries) {
val header = BasicHeader(entry.key.headerTitleRes) val header = BasicHeader(entry.key.headerTitleRes)
list.add(Divider(header)) list.add(Divider(header))
list.add(header) list.add(header)
@ -533,8 +543,9 @@ constructor(
SINGLES(R.string.lbl_singles), SINGLES(R.string.lbl_singles),
COMPILATIONS(R.string.lbl_compilations), COMPILATIONS(R.string.lbl_compilations),
SOUNDTRACKS(R.string.lbl_soundtracks), SOUNDTRACKS(R.string.lbl_soundtracks),
MIXES(R.string.lbl_mixes), DJMIXES(R.string.lbl_mixes),
MIXTAPES(R.string.lbl_mixtapes), MIXTAPES(R.string.lbl_mixtapes),
APPEARANCES(R.string.lbl_appears_on),
LIVE(R.string.lbl_live_group), LIVE(R.string.lbl_live_group),
REMIXES(R.string.lbl_remix_group), REMIXES(R.string.lbl_remix_group),
} }

View file

@ -89,6 +89,7 @@ class AlbumDetailListAdapter(private val listener: Listener<Song>) :
/** /**
* A wrapper around [Disc] signifying that a header should be shown for a disc group. * A wrapper around [Disc] signifying that a header should be shown for a disc group.
*
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
data class DiscHeader(val inner: Disc?) : Item data class DiscHeader(val inner: Disc?) : Item

View file

@ -158,7 +158,7 @@ constructor(
musicSettings.artistSort.artists( musicSettings.artistSort.artists(
if (homeSettings.shouldHideCollaborators) { if (homeSettings.shouldHideCollaborators) {
// Hide Collaborators is enabled, filter out collaborators. // Hide Collaborators is enabled, filter out collaborators.
deviceLibrary.artists.filter { !it.isCollaborator } deviceLibrary.artists.filter { it.explicitAlbums.isNotEmpty() }
} else { } else {
deviceLibrary.artists deviceLibrary.artists
}) })

View file

@ -314,21 +314,23 @@ interface Album : MusicParent {
*/ */
interface Artist : MusicParent { interface Artist : MusicParent {
/** /**
* All of the [Album]s this artist is credited to. Note that any [Song] credited to this artist * All of the [Album]s this artist is credited to from [explicitAlbums] and [implicitAlbums].
* will have it's [Album] considered to be "indirectly" linked to this [Artist], and thus * Note that any [Song] credited to this artist will have it's [Album] considered to be
* included in this list. * "indirectly" linked to this [Artist], and thus included in this list.
*/ */
val albums: List<Album> val albums: List<Album>
/** Albums directly credited to this [Artist] via a "Album Artist" tag. */
val explicitAlbums: List<Album>
/** Albums indirectly credited to this [Artist] via an "Artist" tag. */
val implicitAlbums: List<Album>
/** /**
* The duration of all [Song]s in the artist, in milliseconds. Will be null if there are no * The duration of all [Song]s in the artist, in milliseconds. Will be null if there are no
* songs. * songs.
*/ */
val durationMs: Long? val durationMs: Long?
/**
* Whether this artist is considered a "collaborator", i.e it is not directly credited on any
* [Album].
*/
val isCollaborator: Boolean
/** The [Genre]s of this artist. */ /** The [Genre]s of this artist. */
val genres: List<Genre> val genres: List<Genre>
} }

View file

@ -344,8 +344,9 @@ class ArtistImpl(
override val songs: List<Song> override val songs: List<Song>
override val albums: List<Album> override val albums: List<Album>
override val explicitAlbums: List<Album>
override val implicitAlbums: List<Album>
override val durationMs: Long? override val durationMs: Long?
override val isCollaborator: Boolean
// Note: Append song contents to MusicParent equality so that artists with // Note: Append song contents to MusicParent equality so that artists with
// the same UID but different songs are not equal. // the same UID but different songs are not equal.
@ -366,30 +367,30 @@ class ArtistImpl(
init { init {
val distinctSongs = mutableSetOf<Song>() val distinctSongs = mutableSetOf<Song>()
val distinctAlbums = mutableSetOf<Album>() val albumMap = mutableMapOf<Album, Boolean>()
var noAlbums = true
for (music in songAlbums) { for (music in songAlbums) {
when (music) { when (music) {
is SongImpl -> { is SongImpl -> {
music.link(this) music.link(this)
distinctSongs.add(music) distinctSongs.add(music)
distinctAlbums.add(music.album) if (albumMap[music.album] == null) {
albumMap[music.album] = false
}
} }
is AlbumImpl -> { is AlbumImpl -> {
music.link(this) music.link(this)
distinctAlbums.add(music) albumMap[music] = true
noAlbums = false
} }
else -> error("Unexpected input music ${music::class.simpleName}") else -> error("Unexpected input music ${music::class.simpleName}")
} }
} }
songs = distinctSongs.toList() songs = distinctSongs.toList()
albums = distinctAlbums.toList() albums = albumMap.keys.toList()
explicitAlbums = albumMap.entries.filter { it.value }.map { it.key }
implicitAlbums = albumMap.entries.filterNot { it.value }.map { it.key }
durationMs = songs.sumOf { it.durationMs }.nonZeroOrNull() durationMs = songs.sumOf { it.durationMs }.nonZeroOrNull()
isCollaborator = noAlbums
} }
/** /**

View file

@ -61,14 +61,16 @@
<!-- As in the collection of music --> <!-- As in the collection of music -->
<string name="lbl_mixtape">Mixtape</string> <string name="lbl_mixtape">Mixtape</string>
<!-- As in a compilation of several performances that blend into a single continuous flow of music (Also known as DJ Mixes) --> <!-- As in a compilation of several performances that blend into a single continuous flow of music (Also known as DJ Mixes) -->
<string name="lbl_mixes">Mixes</string> <string name="lbl_mixes">DJ Mixes</string>
<!-- As in a compilation of several performances that blend into a single continuous flow of music (Also known as DJ Mixes) --> <!-- As in a compilation of several performances that blend into a single continuous flow of music (Also known as DJ Mixes) -->
<string name="lbl_mix">Mix</string> <string name="lbl_mix">DJ Mix</string>
<!-- As in music that was performed live --> <!-- As in music that was performed live -->
<string name="lbl_live_group">Live</string> <string name="lbl_live_group">Live</string>
<!-- As in remixed music --> <!-- As in remixed music -->
<string name="lbl_remix_group">Remixes</string> <string name="lbl_remix_group">Remixes</string>
<!-- As in albums that an artist only partially contributed to -->
<string name="lbl_appears_on">Appears on</string>
<string name="lbl_artist">Artist</string> <string name="lbl_artist">Artist</string>
<string name="lbl_artists">Artists</string> <string name="lbl_artists">Artists</string>