Fix bugs w/music loading

Fix bugs with how genres/album names are displayed.
This commit is contained in:
OxygenCobalt 2020-09-17 19:55:55 -06:00
parent ec45a020a5
commit 602cc2153f
14 changed files with 87 additions and 71 deletions

View file

@ -12,7 +12,6 @@ import androidx.navigation.fragment.navArgs
import org.oxycblt.auxio.ClickListener import org.oxycblt.auxio.ClickListener
import org.oxycblt.auxio.databinding.FragmentArtistDetailBinding import org.oxycblt.auxio.databinding.FragmentArtistDetailBinding
import org.oxycblt.auxio.detail.adapters.DetailAlbumAdapter import org.oxycblt.auxio.detail.adapters.DetailAlbumAdapter
import org.oxycblt.auxio.library.DetailViewModel
import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.models.Album import org.oxycblt.auxio.music.models.Album
import org.oxycblt.auxio.theme.applyDivider import org.oxycblt.auxio.theme.applyDivider

View file

@ -1,4 +1,4 @@
package org.oxycblt.auxio.library package org.oxycblt.auxio.detail
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel

View file

@ -24,7 +24,7 @@ class DetailSongAdapter(
holder.bind(data[position]) holder.bind(data[position])
} }
// Generic ViewHolder for an album // Generic ViewHolder for a song
inner class ViewHolder( inner class ViewHolder(
private val binding: ItemAlbumSongBinding private val binding: ItemAlbumSongBinding
) : RecyclerView.ViewHolder(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {

View file

@ -37,13 +37,13 @@ private val ID3_GENRES = arrayOf(
const val PAREN_FILTER = "()" const val PAREN_FILTER = "()"
// Convert legacy ID3 genres to a named genre // Convert legacy ID3 genres to a named genre
fun String.toNamedGenre(): String { fun String.toNamedGenre(): String? {
// Strip the genres of any parentheses, and convert it to an int // Strip the genres of any parentheses, and convert it to an int
val intGenre = this.filterNot { val intGenre = this.filterNot {
PAREN_FILTER.indexOf(it) > -1 PAREN_FILTER.indexOf(it) > -1
}.toInt() }.toInt()
return ID3_GENRES.getOrNull(intGenre) ?: "" return ID3_GENRES.getOrNull(intGenre)
} }
// Convert a song to its URI // Convert a song to its URI

View file

@ -69,20 +69,32 @@ class MusicViewModel(private val app: Application) : ViewModel() {
ioScope.launch { ioScope.launch {
val start = System.currentTimeMillis() val start = System.currentTimeMillis()
val loader = MusicLoader(app.contentResolver) // Get the placeholder strings, which are used by MusicLoader & MusicSorter for
// any music that doesn't have metadata.
val genrePlaceholder = app.getString(R.string.placeholder_genre)
val artistPlaceholder = app.getString(R.string.placeholder_artist)
val albumPlaceholder = app.getString(R.string.placeholder_album)
val loader = MusicLoader(
app.contentResolver,
genrePlaceholder,
artistPlaceholder,
albumPlaceholder
)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (loader.response == MusicLoaderResponse.DONE) { if (loader.response == MusicLoaderResponse.DONE) {
// If the loading succeeds, then process the songs and set them. // If the loading succeeds, then sort the songs and update the value
val sorter = MusicSorter( val sorter = MusicSorter(
loader.genres, loader.genres,
loader.artists, loader.artists,
loader.albums, loader.albums,
loader.songs, loader.songs,
app.getString(R.string.placeholder_unknown_genre), genrePlaceholder,
app.getString(R.string.placeholder_unknown_artist), artistPlaceholder,
app.getString(R.string.placeholder_unknown_album) albumPlaceholder
) )
mSongs.value = sorter.songs.toList() mSongs.value = sorter.songs.toList()

View file

@ -4,9 +4,9 @@ import android.net.Uri
// Abstraction for Song // Abstraction for Song
data class Album( data class Album(
val id: Long = 0L, val id: Long = -1,
var name: String = "", var name: String,
val artistName: String = "", // only used for sorting. Use artist.name instead. val artistName: String,
val coverUri: Uri = Uri.EMPTY, val coverUri: Uri = Uri.EMPTY,
val year: Int = 0 val year: Int = 0
) { ) {

View file

@ -2,9 +2,9 @@ package org.oxycblt.auxio.music.models
// Abstraction for albums // Abstraction for albums
data class Artist( data class Artist(
val id: Long = 0, val id: Long = -1,
var name: String = "", var name: String,
val genres: MutableList<Genre> = mutableListOf(Genre()) val genres: MutableList<Genre> = mutableListOf()
) { ) {
val albums = mutableListOf<Album>() val albums = mutableListOf<Album>()
var genre = "" var genre = ""

View file

@ -1,8 +1,8 @@
package org.oxycblt.auxio.music.models package org.oxycblt.auxio.music.models
data class Genre( data class Genre(
val id: Long = 0, val id: Long = -1,
var name: String = "", var name: String,
) { ) {
val artists = mutableListOf<Artist>() val artists = mutableListOf<Artist>()

View file

@ -6,12 +6,12 @@ import android.text.format.DateUtils
data class Song( data class Song(
val id: Long, val id: Long,
var name: String, var name: String,
val albumName: String, // Only used for sorting. Use album.title for everything else. val albumId: Long,
val track: Int, val track: Int,
val duration: Long val duration: Long
) { ) {
lateinit var album: Album lateinit var album: Album
val seconds = duration / 1000 val seconds = duration / 1000
val formattedDuration = DateUtils.formatElapsedTime(seconds) val formattedDuration: String = DateUtils.formatElapsedTime(seconds)
} }

View file

@ -19,7 +19,13 @@ enum class MusicLoaderResponse {
} }
// Class that loads music from the FileSystem. // Class that loads music from the FileSystem.
class MusicLoader(private val resolver: ContentResolver) { class MusicLoader(
private val resolver: ContentResolver,
private val genrePlaceholder: String,
private val artistPlaceholder: String,
private val albumPlaceholder: String,
) {
var genres = mutableListOf<Genre>() var genres = mutableListOf<Genre>()
var artists = mutableListOf<Artist>() var artists = mutableListOf<Artist>()
@ -78,12 +84,12 @@ class MusicLoader(private val resolver: ContentResolver) {
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
val id = cursor.getLong(idIndex) val id = cursor.getLong(idIndex)
var name = cursor.getString(nameIndex) ?: "" var name = cursor.getString(nameIndex) ?: genrePlaceholder
// If a genre is still in an old int-based format [Android formats it as "(INT)"], // If a genre is still in an old int-based format [Android formats it as "(INT)"],
// convert that to the corresponding ID3 genre. // convert that to the corresponding ID3 genre.
if (name.contains(Regex("[0123456789)]"))) { if (name.contains(Regex("[0123456789)]"))) {
name = name.toNamedGenre() name = name.toNamedGenre() ?: genrePlaceholder
} }
genres.add( genres.add(
@ -124,7 +130,7 @@ class MusicLoader(private val resolver: ContentResolver) {
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
val id = cursor.getLong(idIndex) val id = cursor.getLong(idIndex)
val name = cursor.getString(nameIndex) ?: "" val name = cursor.getString(nameIndex) ?: artistPlaceholder
// If an artist has already been added [Which is very likely due to how genres // If an artist has already been added [Which is very likely due to how genres
// are processed], add the genre to the existing artist instead of creating a // are processed], add the genre to the existing artist instead of creating a
@ -182,12 +188,16 @@ class MusicLoader(private val resolver: ContentResolver) {
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
val id = cursor.getLong(idIndex) val id = cursor.getLong(idIndex)
val name = cursor.getString(nameIndex) ?: "" var name = cursor.getString(nameIndex) ?: albumPlaceholder
val artist = cursor.getString(artistIndex) ?: "" val artist = cursor.getString(artistIndex)
val year = cursor.getInt(yearIndex) val year = cursor.getInt(yearIndex)
val coverUri = id.toAlbumArtURI() val coverUri = id.toAlbumArtURI()
// Sometimes the android system will return 0 for an album name, update
// it properly if that happens.
name = if (name == "0") albumPlaceholder else name
albums.add( albums.add(
Album( Album(
id, name, artist, id, name, artist,
@ -219,9 +229,9 @@ class MusicLoader(private val resolver: ContentResolver) {
Media._ID, // 0 Media._ID, // 0
Media.DISPLAY_NAME, // 1 Media.DISPLAY_NAME, // 1
Media.TITLE, // 2 Media.TITLE, // 2
Media.ALBUM, // 4 Media.ALBUM_ID, // 3
Media.TRACK, // 6 Media.TRACK, // 4
Media.DURATION // 7 Media.DURATION // 5
), ),
Media.IS_MUSIC + "=1", null, Media.IS_MUSIC + "=1", null,
Media.DEFAULT_SORT_ORDER Media.DEFAULT_SORT_ORDER
@ -231,20 +241,20 @@ class MusicLoader(private val resolver: ContentResolver) {
val idIndex = cursor.getColumnIndexOrThrow(Media._ID) val idIndex = cursor.getColumnIndexOrThrow(Media._ID)
val fileIndex = cursor.getColumnIndexOrThrow(Media.DISPLAY_NAME) val fileIndex = cursor.getColumnIndexOrThrow(Media.DISPLAY_NAME)
val titleIndex = cursor.getColumnIndexOrThrow(Media.TITLE) val titleIndex = cursor.getColumnIndexOrThrow(Media.TITLE)
val albumIndex = cursor.getColumnIndexOrThrow(Media.ALBUM) val albumIndex = cursor.getColumnIndexOrThrow(Media.ALBUM_ID)
val trackIndex = cursor.getColumnIndexOrThrow(Media.TRACK) val trackIndex = cursor.getColumnIndexOrThrow(Media.TRACK)
val durationIndex = cursor.getColumnIndexOrThrow(Media.DURATION) val durationIndex = cursor.getColumnIndexOrThrow(Media.DURATION)
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
val id = cursor.getLong(idIndex) val id = cursor.getLong(idIndex)
val title = cursor.getString(titleIndex) ?: cursor.getString(fileIndex) val title = cursor.getString(titleIndex) ?: cursor.getString(fileIndex)
val album = cursor.getString(albumIndex) ?: "" val albumId = cursor.getLong(albumIndex)
val track = cursor.getInt(trackIndex) val track = cursor.getInt(trackIndex)
val duration = cursor.getLong(durationIndex) val duration = cursor.getLong(durationIndex)
songs.add( songs.add(
Song( Song(
id, title, album, id, title, albumId,
track, duration track, duration
) )
) )
@ -255,7 +265,7 @@ class MusicLoader(private val resolver: ContentResolver) {
// Remove dupes // Remove dupes
songs = songs.distinctBy { songs = songs.distinctBy {
it.name to it.albumName to it.track to it.duration it.name to it.albumId to it.track to it.duration
}.toMutableList() }.toMutableList()
Log.d( Log.d(

View file

@ -21,7 +21,6 @@ class MusicSorter(
sortAlbumsIntoArtists() sortAlbumsIntoArtists()
sortArtistsIntoGenres() sortArtistsIntoGenres()
addPlaceholders()
finalizeMusic() finalizeMusic()
} }
@ -31,8 +30,9 @@ class MusicSorter(
val unknownSongs = songs.toMutableList() val unknownSongs = songs.toMutableList()
for (album in albums) { for (album in albums) {
// Find all songs that match the current album title // Find all songs that match the current album ID to prevent any bugs w/comparing names.
val albumSongs = songs.filter { it.albumName == album.name } // This cant be done with artists/genres sadly.
val albumSongs = songs.filter { it.albumId == album.id }
// Then add them to the album // Then add them to the album
for (song in albumSongs) { for (song in albumSongs) {
@ -40,8 +40,6 @@ class MusicSorter(
album.songs.add(song) album.songs.add(song)
} }
album.finalize()
unknownSongs.removeAll(albumSongs) unknownSongs.removeAll(albumSongs)
} }
@ -49,15 +47,16 @@ class MusicSorter(
if (unknownSongs.size > 0) { if (unknownSongs.size > 0) {
// Reuse an existing unknown album if one is found // Reuse an existing unknown album if one is found
val unknownAlbum = albums.find { it.name == "" } ?: Album() val unknownAlbum = Album(
name = albumPlaceholder,
artistName = artistPlaceholder
)
for (song in unknownSongs) { for (song in unknownSongs) {
song.album = unknownAlbum song.album = unknownAlbum
unknownAlbum.songs.add(song) unknownAlbum.songs.add(song)
} }
unknownAlbum.finalize()
albums.add(unknownAlbum) albums.add(unknownAlbum)
Log.d( Log.d(
@ -76,14 +75,14 @@ class MusicSorter(
// Find all albums that match the current artist name // Find all albums that match the current artist name
val artistAlbums = albums.filter { it.artistName == artist.name } val artistAlbums = albums.filter { it.artistName == artist.name }
Log.d(this::class.simpleName, artist.id.toString())
// Then add them to the artist, along with refreshing the amount of albums // Then add them to the artist, along with refreshing the amount of albums
for (album in artistAlbums) { for (album in artistAlbums) {
album.artist = artist album.artist = artist
artist.albums.add(album) artist.albums.add(album)
} }
artist.finalize()
unknownAlbums.removeAll(artistAlbums) unknownAlbums.removeAll(artistAlbums)
} }
@ -91,15 +90,15 @@ class MusicSorter(
if (unknownAlbums.size > 0) { if (unknownAlbums.size > 0) {
// Reuse an existing unknown artist if one is found // Reuse an existing unknown artist if one is found
val unknownArtist = artists.find { it.name == "" } ?: Artist() val unknownArtist = Artist(
name = artistPlaceholder
)
for (album in unknownAlbums) { for (album in unknownAlbums) {
album.artist = unknownArtist album.artist = unknownArtist
unknownArtist.albums.add(album) unknownArtist.albums.add(album)
} }
unknownArtist.finalize()
artists.add(unknownArtist) artists.add(unknownArtist)
Log.d( Log.d(
@ -124,40 +123,39 @@ class MusicSorter(
// Then add them to the genre, along with refreshing the amount of artists // Then add them to the genre, along with refreshing the amount of artists
genre.artists.addAll(genreArtists) genre.artists.addAll(genreArtists)
genre.finalize()
unknownArtists.removeAll(genreArtists) unknownArtists.removeAll(genreArtists)
} }
if (unknownArtists.size > 0) { if (unknownArtists.size > 0) {
// Reuse an existing unknown genre if one is found // Reuse an existing unknown genre if one is found
val unknownGenre = genres.find { it.name == "" } ?: Genre() val unknownGenre = Genre(
name = genrePlaceholder
)
for (artist in unknownArtists) { for (artist in unknownArtists) {
artist.genres.add(unknownGenre) artist.genres.add(unknownGenre)
unknownGenre.artists.add(artist) unknownGenre.artists.add(artist)
} }
unknownGenre.finalize()
genres.add(unknownGenre) genres.add(unknownGenre)
Log.d( Log.d(
this::class.simpleName, this::class.simpleName,
"${unknownArtists.size} albums were placed into an unknown genre." "${unknownArtists.size} artists were placed into an unknown genre."
) )
} }
} }
// Correct any empty names [""] with the proper placeholders [Unknown Album] // Finalize music
private fun addPlaceholders() {
genres.forEach { if (it.name == "") it.name = genrePlaceholder }
artists.forEach { if (it.name == "") it.name = artistPlaceholder }
albums.forEach { if (it.name == "") it.name = albumPlaceholder }
}
// Sort all music
private fun finalizeMusic() { private fun finalizeMusic() {
// Correct any empty names [""] with the proper placeholders [Unknown Album]
genres.forEach { it.finalize() }
artists.forEach { it.finalize() }
albums.forEach { it.finalize() }
// Then finally sort the music
genres.sortWith( genres.sortWith(
compareBy(String.CASE_INSENSITIVE_ORDER, { it.name }) compareBy(String.CASE_INSENSITIVE_ORDER, { it.name })
) )

View file

@ -90,7 +90,7 @@
tools:text="2020" /> tools:text="2020" />
<TextView <TextView
android:id="@+id/header_songs" android:id="@+id/header"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:text="@string/label_songs" android:text="@string/label_songs"
@ -115,10 +115,8 @@
android:overScrollMode="never" android:overScrollMode="never"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/header_songs" app:layout_constraintTop_toBottomOf="@+id/header"
app:layout_constraintVertical_bias="0.0"
tools:itemCount="4" tools:itemCount="4"
tools:layout_editor_absoluteX="0dp"
tools:listitem="@layout/item_album_song" /> tools:listitem="@layout/item_album_song" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -90,7 +90,7 @@
tools:text="2 Albums, 20 Songs" /> tools:text="2 Albums, 20 Songs" />
<TextView <TextView
android:id="@+id/header_albums" android:id="@+id/header"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:text="@string/label_albums" android:text="@string/label_albums"
@ -103,8 +103,7 @@
android:paddingTop="@dimen/padding_small" android:paddingTop="@dimen/padding_small"
android:paddingBottom="@dimen/padding_small" android:paddingBottom="@dimen/padding_small"
android:background="@drawable/header_dividers" android:background="@drawable/header_dividers"
app:layout_constraintTop_toBottomOf="@+id/artist_counts" app:layout_constraintTop_toBottomOf="@+id/artist_counts" />
tools:layout_editor_absoluteX="60dp" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/album_recycler" android:id="@+id/album_recycler"
@ -115,7 +114,7 @@
android:overScrollMode="never" android:overScrollMode="never"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/header_albums" app:layout_constraintTop_toBottomOf="@+id/header"
tools:itemCount="4" tools:itemCount="4"
tools:listitem="@layout/item_album" /> tools:listitem="@layout/item_album" />

View file

@ -14,9 +14,9 @@
<string name="label_albums">Albums</string> <string name="label_albums">Albums</string>
<string name="label_songs">Songs</string> <string name="label_songs">Songs</string>
<string name="placeholder_unknown_genre">Unknown Genre</string> <string name="placeholder_genre">Unknown Genre</string>
<string name="placeholder_unknown_artist">Unknown Artist</string> <string name="placeholder_artist">Unknown Artist</string>
<string name="placeholder_unknown_album">Unknown Album</string> <string name="placeholder_album">Unknown Album</string>
<string name="format_info">%1$s / %2$s</string> <string name="format_info">%1$s / %2$s</string>
<string name="format_double_counts">%1$s, %2$s</string> <string name="format_double_counts">%1$s, %2$s</string>