Rewrite Music Loading a g a i n
Rewrite the music loading for [hopefully] the final time, now with Artists/Albums/Media instead of just AudioColumns, improved genre loading, and easier to implement album-art. Hopefully this system will stick.
This commit is contained in:
parent
160013bbe9
commit
b1be2802cf
9 changed files with 366 additions and 215 deletions
38
app/src/main/java/org/oxycblt/auxio/music/GenreCompat.kt
Normal file
38
app/src/main/java/org/oxycblt/auxio/music/GenreCompat.kt
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
package org.oxycblt.auxio.music
|
||||||
|
|
||||||
|
// Compatibility layer to convert old int-based genres to new genres
|
||||||
|
val ID3_GENRES = arrayOf<String>(
|
||||||
|
"Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", "Hip-Hop", "Jazz",
|
||||||
|
"Metal", "New Age", "Oldies", "Other", "Pop", "R&B", "Rap", "Reggae", "Rock", "Techno",
|
||||||
|
"Industrial", "Alternative", "Ska", "Death Metal", "Pranks", "Soundtrack", "Euro-Techno",
|
||||||
|
"Ambient", "Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical", "Instrumental",
|
||||||
|
"Acid", "House", "Game", "Sound Clip", "Gospel", "Noise", "AlternRock", "Bass", "Soul", "Punk",
|
||||||
|
"Space", "Meditative", "Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic", "Darkwave",
|
||||||
|
"Techno-Industrial", "Electronic", "Pop-Folk", "Eurodance", "Dream", "Southern Rock", "Comedy",
|
||||||
|
"Cult", "Gangsta", "Top 40", "Christian Rap", "Pop/Funk", "Jungle", "Native American",
|
||||||
|
"Cabaret", "New Wave", "Psychadelic", "Rave", "Showtunes", "Trailer", "Lo-Fi", "Tribal",
|
||||||
|
"Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical", "Rock & Roll", "Hard Rock",
|
||||||
|
|
||||||
|
// Winamp extensions
|
||||||
|
"Folk", "Folk-Rock", "National Folk", "Swing", "Fast Fusion", "Bebob", "Latin", "Revival",
|
||||||
|
"Celtic", "Bluegrass", "Avantgarde", "Gothic Rock", "Progressive Rock", "Psychedelic Rock",
|
||||||
|
"Symphonic Rock", "Slow Rock", "Big Band", "Chorus", "Easy Listening", "Acoustic", "Humour",
|
||||||
|
"Speech", "Chanson", "Opera", "Chamber Music", "Sonata", "Symphony", "Booty Bass", "Primus",
|
||||||
|
"Porn Groove", "Satire", "Slow Jam", "Club", "Tango", "Samba", "Folklore", "Ballad",
|
||||||
|
"Power Ballad", "Rhythmic Soul", "Freestyle", "Duet", "Punk Rock", "Drum Solo", "A capella",
|
||||||
|
"Euro-House", "Dance Hall", "Goa", "Drum & Bass", "Club-House", "Hardcore", "Terror", "Indie",
|
||||||
|
"Britpop", "Negerpunk", "Polsk Punk", "Beat", "Christian Gangsta", "Heavy Metal", "Black Metal",
|
||||||
|
"Crossover", "Contemporary Christian", "Christian Rock", "Merengue", "Salsa", "Thrash Metal",
|
||||||
|
"Anime", "JPop", "Synthpop"
|
||||||
|
)
|
||||||
|
|
||||||
|
const val PAREN_FILTER = "()"
|
||||||
|
|
||||||
|
fun intToNamedGenre(genre: String): String? {
|
||||||
|
// Strip the genres of any parentheses, and convert it to an int
|
||||||
|
val intGenre = genre.filterNot {
|
||||||
|
PAREN_FILTER.indexOf(it) > -1
|
||||||
|
}.toInt()
|
||||||
|
|
||||||
|
return ID3_GENRES.getOrNull(intGenre)
|
||||||
|
}
|
|
@ -3,10 +3,14 @@ package org.oxycblt.auxio.music
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore.Audio.Albums
|
||||||
|
import android.provider.MediaStore.Audio.Artists
|
||||||
import android.provider.MediaStore.Audio.Genres
|
import android.provider.MediaStore.Audio.Genres
|
||||||
import android.provider.MediaStore.Audio.AudioColumns
|
import android.provider.MediaStore.Audio.Media
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import org.oxycblt.auxio.music.models.Album
|
||||||
|
import org.oxycblt.auxio.music.models.Artist
|
||||||
|
import org.oxycblt.auxio.music.models.Genre
|
||||||
import org.oxycblt.auxio.music.models.Song
|
import org.oxycblt.auxio.music.models.Song
|
||||||
|
|
||||||
enum class MusicLoaderResponse {
|
enum class MusicLoaderResponse {
|
||||||
|
@ -17,62 +21,44 @@ enum class MusicLoaderResponse {
|
||||||
// FIXME: This thing probably has some memory leaks *somewhere*
|
// FIXME: This thing probably has some memory leaks *somewhere*
|
||||||
class MusicLoader(private val app: Application) {
|
class MusicLoader(private val app: Application) {
|
||||||
|
|
||||||
|
var genres = mutableListOf<Genre>()
|
||||||
|
var artists = mutableListOf<Artist>()
|
||||||
|
var albums = mutableListOf<Album>()
|
||||||
var songs = mutableListOf<Song>()
|
var songs = mutableListOf<Song>()
|
||||||
var genres = mutableListOf<Pair<Long, String>>()
|
|
||||||
|
|
||||||
private var musicCursor: Cursor? = null
|
|
||||||
private var genreCursor: Cursor? = null
|
private var genreCursor: Cursor? = null
|
||||||
|
private var artistCursor: Cursor? = null
|
||||||
|
private var albumCursor: Cursor? = null
|
||||||
|
private var songCursor: Cursor? = null
|
||||||
|
|
||||||
val response: MusicLoaderResponse
|
val response: MusicLoaderResponse
|
||||||
|
val resolver: ContentResolver = app.contentResolver
|
||||||
|
|
||||||
init {
|
init {
|
||||||
response = findMusic()
|
response = findMusic()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findMusic(): MusicLoaderResponse {
|
private fun findMusic(): MusicLoaderResponse {
|
||||||
genreCursor = getGenreCursor(
|
|
||||||
app.contentResolver
|
|
||||||
)
|
|
||||||
|
|
||||||
useGenreCursor()
|
|
||||||
|
|
||||||
indexMusic()
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
loadGenres()
|
||||||
|
loadArtists()
|
||||||
|
loadAlbums()
|
||||||
|
loadSongs()
|
||||||
} catch (error: Exception) {
|
} catch (error: Exception) {
|
||||||
Log.e(this::class.simpleName, "Something went horribly wrong.")
|
Log.e(this::class.simpleName, "Something went horribly wrong.")
|
||||||
error.printStackTrace()
|
error.printStackTrace()
|
||||||
|
|
||||||
musicCursor?.close()
|
|
||||||
|
|
||||||
return MusicLoaderResponse.FAILURE
|
return MusicLoaderResponse.FAILURE
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the main loading completed without a failure, return DONE or
|
return MusicLoaderResponse.DONE
|
||||||
// NO_MUSIC depending on if any music was found.
|
|
||||||
return if (songs.size > 0) {
|
|
||||||
Log.d(
|
|
||||||
this::class.simpleName,
|
|
||||||
"Successfully found " + songs.size.toString() + " Songs."
|
|
||||||
)
|
|
||||||
|
|
||||||
MusicLoaderResponse.DONE
|
|
||||||
} else {
|
|
||||||
Log.d(
|
|
||||||
this::class.simpleName,
|
|
||||||
"No music was found."
|
|
||||||
)
|
|
||||||
|
|
||||||
MusicLoaderResponse.NO_MUSIC
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getGenreCursor(resolver: ContentResolver): Cursor? {
|
private fun loadGenres() {
|
||||||
Log.i(this::class.simpleName, "Getting genre cursor.")
|
Log.d(this::class.simpleName, "Starting genre search...")
|
||||||
|
|
||||||
// Get every Genre indexed by the android system, for some reason
|
// First, get a cursor for every genre in the android system
|
||||||
// you cant directly get this from the plain music cursor.
|
genreCursor = resolver.query(
|
||||||
return resolver.query(
|
|
||||||
Genres.EXTERNAL_CONTENT_URI,
|
Genres.EXTERNAL_CONTENT_URI,
|
||||||
arrayOf(
|
arrayOf(
|
||||||
Genres._ID, // 0
|
Genres._ID, // 0
|
||||||
|
@ -81,116 +67,212 @@ class MusicLoader(private val app: Application) {
|
||||||
null, null,
|
null, null,
|
||||||
Genres.DEFAULT_SORT_ORDER
|
Genres.DEFAULT_SORT_ORDER
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
private fun getMusicCursor(resolver: ContentResolver, genreId: Long): Cursor? {
|
|
||||||
Log.i(this::class.simpleName, "Getting music cursor.")
|
|
||||||
|
|
||||||
// Get all the values that can be retrieved by the cursor.
|
|
||||||
// The rest are retrieved using MediaMetadataExtractor and
|
|
||||||
// stored into a database.
|
|
||||||
return resolver.query(
|
|
||||||
Genres.Members.getContentUri("external", genreId),
|
|
||||||
arrayOf(
|
|
||||||
AudioColumns._ID, // 0
|
|
||||||
AudioColumns.DISPLAY_NAME, // 1
|
|
||||||
AudioColumns.TITLE, // 2
|
|
||||||
AudioColumns.ARTIST, // 3
|
|
||||||
AudioColumns.ALBUM, // 4
|
|
||||||
AudioColumns.YEAR, // 5
|
|
||||||
AudioColumns.TRACK, // 6
|
|
||||||
AudioColumns.DURATION // 7
|
|
||||||
),
|
|
||||||
AudioColumns.IS_MUSIC + "=1", null,
|
|
||||||
MediaStore.Audio.Media.DEFAULT_SORT_ORDER
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the genre cursor to index all the genres the android system has indexed.
|
|
||||||
private fun useGenreCursor() {
|
|
||||||
// TODO: Move genre to its own model, even if its just two values
|
|
||||||
|
|
||||||
|
// And then process those into Genre objects
|
||||||
genreCursor?.use { cursor ->
|
genreCursor?.use { cursor ->
|
||||||
|
|
||||||
// Don't even bother running if there's nothing to process.
|
|
||||||
if (cursor.count == 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val idIndex = cursor.getColumnIndexOrThrow(Genres._ID)
|
val idIndex = cursor.getColumnIndexOrThrow(Genres._ID)
|
||||||
val nameIndex = cursor.getColumnIndexOrThrow(Genres.NAME)
|
val nameIndex = cursor.getColumnIndexOrThrow(Genres.NAME)
|
||||||
|
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
genres.add(
|
val id = cursor.getLong(idIndex)
|
||||||
Pair<Long, String>(
|
var name = cursor.getString(nameIndex)
|
||||||
cursor.getLong(idIndex),
|
|
||||||
cursor.getString(nameIndex)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
Log.i(this::class.simpleName, cursor.getString(nameIndex))
|
// If a genre is still in an old int-based format [Android formats it as "(INT)"],
|
||||||
|
// convert that to the corresponding ID3 genre.
|
||||||
|
if (name.contains("[0-9][()]")) {
|
||||||
|
name = intToNamedGenre(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
genres.add(
|
||||||
|
Genre(
|
||||||
|
id, name
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor.close()
|
cursor.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.i(this::class.simpleName, "Retrieved " + genres.size.toString() + " Genres.")
|
// Remove dupes
|
||||||
|
genres = genres.distinctBy {
|
||||||
|
it.name
|
||||||
|
}.toMutableList()
|
||||||
|
|
||||||
|
Log.d(
|
||||||
|
this::class.simpleName,
|
||||||
|
"Genre search finished with " +
|
||||||
|
genres.size.toString() +
|
||||||
|
" genres found."
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the cursor index music files from the shared storage.
|
private fun loadArtists() {
|
||||||
private fun indexMusic() {
|
Log.d(this::class.simpleName, "Starting artist search...")
|
||||||
Log.i(this::class.simpleName, "Starting music search...")
|
|
||||||
|
|
||||||
// So, android has a brain aneurysm if you try to get an audio genre through
|
// To associate artists with their genres, a new cursor is
|
||||||
// AudioColumns. As a result, I just index every genre's name & ID, create a cursor
|
// created with all the artists of that type.
|
||||||
// of music for that genres ID, and then load and iterate through them normally,
|
|
||||||
// creating using the genres name as that songs Genre.
|
|
||||||
|
|
||||||
// God why do I have to do this
|
|
||||||
for (genre in genres) {
|
for (genre in genres) {
|
||||||
val musicCursor = getMusicCursor(app.contentResolver, genre.first)
|
artistCursor = resolver.query(
|
||||||
|
Genres.Members.getContentUri("external", genre.id),
|
||||||
|
arrayOf(
|
||||||
|
Artists._ID,
|
||||||
|
Artists.ARTIST
|
||||||
|
),
|
||||||
|
null, null,
|
||||||
|
Artists.DEFAULT_SORT_ORDER
|
||||||
|
)
|
||||||
|
|
||||||
musicCursor?.use { cursor ->
|
artistCursor?.use { cursor ->
|
||||||
|
val idIndex = cursor.getColumnIndexOrThrow(Artists._ID)
|
||||||
// Don't run the more expensive file loading operations if there
|
val nameIndex = cursor.getColumnIndexOrThrow(Artists.ARTIST)
|
||||||
// is no music to index.
|
|
||||||
if (cursor.count == 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val idIndex = cursor.getColumnIndexOrThrow(AudioColumns._ID)
|
|
||||||
val displayIndex = cursor.getColumnIndexOrThrow(AudioColumns.DISPLAY_NAME)
|
|
||||||
val titleIndex = cursor.getColumnIndexOrThrow(AudioColumns.TITLE)
|
|
||||||
val artistIndex = cursor.getColumnIndexOrThrow(AudioColumns.ARTIST)
|
|
||||||
val albumIndex = cursor.getColumnIndexOrThrow(AudioColumns.ALBUM)
|
|
||||||
val yearIndex = cursor.getColumnIndexOrThrow(AudioColumns.YEAR)
|
|
||||||
val trackIndex = cursor.getColumnIndexOrThrow(AudioColumns.TRACK)
|
|
||||||
val durationIndex = cursor.getColumnIndexOrThrow(AudioColumns.DURATION)
|
|
||||||
|
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
val id = cursor.getLong(idIndex)
|
val id = cursor.getLong(idIndex)
|
||||||
val display = cursor.getString(displayIndex)
|
val name = cursor.getString(nameIndex)
|
||||||
|
|
||||||
// Get the basic metadata from the cursor
|
// If an artist has already been added [Which is very likely due to how genres
|
||||||
val title = cursor.getString(titleIndex) ?: display
|
// are processed], add the genre to the existing artist instead of creating a
|
||||||
|
// new one.
|
||||||
|
val existingArtist = artists.find { it.name == name }
|
||||||
|
|
||||||
|
if (existingArtist != null) {
|
||||||
|
existingArtist.genres.add(genre.name)
|
||||||
|
} else {
|
||||||
|
artists.add(
|
||||||
|
Artist(
|
||||||
|
id, name,
|
||||||
|
mutableListOf(genre.name)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove dupes [Just in case]
|
||||||
|
artists = artists.distinctBy {
|
||||||
|
it.name to it.genres
|
||||||
|
}.toMutableList()
|
||||||
|
|
||||||
|
Log.d(
|
||||||
|
this::class.simpleName,
|
||||||
|
"Artist search finished with " +
|
||||||
|
artists.size.toString() +
|
||||||
|
" artists found."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadAlbums() {
|
||||||
|
Log.d(this::class.simpleName, "Starting album search...")
|
||||||
|
|
||||||
|
albumCursor = resolver.query(
|
||||||
|
Albums.EXTERNAL_CONTENT_URI,
|
||||||
|
arrayOf(
|
||||||
|
Albums._ID,
|
||||||
|
Albums.ALBUM,
|
||||||
|
Albums.ARTIST,
|
||||||
|
|
||||||
|
// FIXME: May be an issue for albums whose songs released in multiple years
|
||||||
|
Albums.FIRST_YEAR,
|
||||||
|
Albums.NUMBER_OF_SONGS
|
||||||
|
),
|
||||||
|
null, null,
|
||||||
|
Albums.DEFAULT_SORT_ORDER
|
||||||
|
)
|
||||||
|
|
||||||
|
albumCursor?.use { cursor ->
|
||||||
|
val idIndex = cursor.getColumnIndexOrThrow(Albums._ID)
|
||||||
|
val nameIndex = cursor.getColumnIndexOrThrow(Albums.ALBUM)
|
||||||
|
val artistIndex = cursor.getColumnIndexOrThrow(Albums.ARTIST)
|
||||||
|
val yearIndex = cursor.getColumnIndexOrThrow(Albums.FIRST_YEAR)
|
||||||
|
val numIndex = cursor.getColumnIndexOrThrow(Albums.NUMBER_OF_SONGS)
|
||||||
|
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
val id = cursor.getLong(idIndex)
|
||||||
|
val name = cursor.getString(nameIndex)
|
||||||
val artist = cursor.getString(artistIndex)
|
val artist = cursor.getString(artistIndex)
|
||||||
val album = cursor.getString(albumIndex)
|
|
||||||
val year = cursor.getInt(yearIndex)
|
val year = cursor.getInt(yearIndex)
|
||||||
val track = cursor.getInt(trackIndex)
|
val numSongs = cursor.getInt(numIndex)
|
||||||
val duration = cursor.getLong(durationIndex)
|
|
||||||
|
|
||||||
// TODO: Add album art [But its loaded separately, as that will take a bit]
|
albums.add(
|
||||||
// TODO: Add genres whenever android hasn't borked it
|
Album(
|
||||||
songs.add(
|
id, name, artist,
|
||||||
Song(
|
year, numSongs
|
||||||
id, title, artist, album,
|
|
||||||
genre.second, year, track, duration
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor.close()
|
cursor.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove dupes
|
||||||
|
albums = albums.distinctBy {
|
||||||
|
it.title to it.artistName to it.year to it.numSongs
|
||||||
|
}.toMutableList()
|
||||||
|
|
||||||
|
Log.d(
|
||||||
|
this::class.simpleName,
|
||||||
|
"Album search finished with " +
|
||||||
|
albums.size.toString() +
|
||||||
|
" albums found."
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun loadSongs() {
|
||||||
|
Log.d(this::class.simpleName, "Starting song search...")
|
||||||
|
|
||||||
|
songCursor = resolver.query(
|
||||||
|
Media.EXTERNAL_CONTENT_URI,
|
||||||
|
arrayOf(
|
||||||
|
Media._ID, // 0
|
||||||
|
Media.DISPLAY_NAME, // 1
|
||||||
|
Media.TITLE, // 2
|
||||||
|
Media.ALBUM, // 4
|
||||||
|
Media.TRACK, // 6
|
||||||
|
Media.DURATION // 7
|
||||||
|
),
|
||||||
|
Media.IS_MUSIC + "=1", null,
|
||||||
|
Media.DEFAULT_SORT_ORDER
|
||||||
|
)
|
||||||
|
|
||||||
|
songCursor?.use { cursor ->
|
||||||
|
val idIndex = cursor.getColumnIndexOrThrow(Media._ID)
|
||||||
|
val fileIndex = cursor.getColumnIndexOrThrow(Media.DISPLAY_NAME)
|
||||||
|
val titleIndex = cursor.getColumnIndexOrThrow(Media.TITLE)
|
||||||
|
val albumIndex = cursor.getColumnIndexOrThrow(Media.ALBUM)
|
||||||
|
val trackIndex = cursor.getColumnIndexOrThrow(Media.TRACK)
|
||||||
|
val durationIndex = cursor.getColumnIndexOrThrow(Media.DURATION)
|
||||||
|
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
val id = cursor.getLong(idIndex)
|
||||||
|
val title = cursor.getString(titleIndex) ?: cursor.getString(fileIndex)
|
||||||
|
val album = cursor.getString(albumIndex)
|
||||||
|
val track = cursor.getInt(trackIndex)
|
||||||
|
val duration = cursor.getLong(durationIndex)
|
||||||
|
|
||||||
|
songs.add(
|
||||||
|
Song(
|
||||||
|
id, title, album,
|
||||||
|
track, duration
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove dupes
|
||||||
|
songs = songs.distinctBy {
|
||||||
|
it.title to it.albumName to it.track to it.duration
|
||||||
|
}.toMutableList()
|
||||||
|
|
||||||
|
Log.d(
|
||||||
|
this::class.simpleName,
|
||||||
|
"Song search finished with " +
|
||||||
|
songs.size.toString() +
|
||||||
|
" songs found."
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,15 +22,25 @@ class MusicRepository {
|
||||||
val songs: LiveData<List<Song>> get() = mSongs
|
val songs: LiveData<List<Song>> get() = mSongs
|
||||||
|
|
||||||
suspend fun init(app: Application): MusicLoaderResponse {
|
suspend fun init(app: Application): MusicLoaderResponse {
|
||||||
|
Log.i(this::class.simpleName, "Starting initial music load")
|
||||||
|
|
||||||
val loader = MusicLoader(app)
|
val loader = MusicLoader(app)
|
||||||
|
|
||||||
if (loader.response == MusicLoaderResponse.DONE) {
|
if (loader.response == MusicLoaderResponse.DONE) {
|
||||||
// If the loading succeeds, then process the songs into lists of
|
// If the loading succeeds, then process the songs into lists of
|
||||||
// songs, albums, and artists on the main thread.
|
// songs, albums, and artists on the main thread.
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
mSongs.value = processSongs(loader.songs)
|
val sorter = MusicSorter(
|
||||||
mAlbums.value = sortIntoAlbums(mSongs.value as MutableList<Song>)
|
loader.artists,
|
||||||
mArtists.value = sortIntoArtists(mAlbums.value as MutableList<Album>)
|
loader.albums,
|
||||||
|
loader.songs
|
||||||
|
)
|
||||||
|
|
||||||
|
mSongs.value = sorter.songs
|
||||||
|
mAlbums.value = sorter.albums
|
||||||
|
mArtists.value = sorter.artists
|
||||||
|
|
||||||
|
Log.i(this::class.simpleName, "Finished initial music load.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
92
app/src/main/java/org/oxycblt/auxio/music/MusicSorter.kt
Normal file
92
app/src/main/java/org/oxycblt/auxio/music/MusicSorter.kt
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
package org.oxycblt.auxio.music
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import org.oxycblt.auxio.music.models.Album
|
||||||
|
import org.oxycblt.auxio.music.models.Artist
|
||||||
|
import org.oxycblt.auxio.music.models.Song
|
||||||
|
|
||||||
|
class MusicSorter(
|
||||||
|
val artists: MutableList<Artist>,
|
||||||
|
val albums: MutableList<Album>,
|
||||||
|
val songs: MutableList<Song>
|
||||||
|
) {
|
||||||
|
init {
|
||||||
|
sortSongsIntoAlbums()
|
||||||
|
sortAlbumsIntoArtists()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sortSongsIntoAlbums() {
|
||||||
|
Log.d(this::class.simpleName, "Sorting songs into albums...")
|
||||||
|
|
||||||
|
val unknownSongs = songs.toMutableList()
|
||||||
|
|
||||||
|
for (album in albums) {
|
||||||
|
// Find all songs that match the current album title
|
||||||
|
val albumSongs = songs.filter { it.albumName == album.title }
|
||||||
|
|
||||||
|
// And then add them to the album
|
||||||
|
album.songs.addAll(albumSongs)
|
||||||
|
|
||||||
|
unknownSongs.removeAll(albumSongs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any remaining songs will be added to an unknown album
|
||||||
|
if (unknownSongs.size > 0) {
|
||||||
|
|
||||||
|
// Reuse an existing unknown albumif one is found
|
||||||
|
val unknownAlbum = albums.find { it.title == null } ?: Album()
|
||||||
|
|
||||||
|
unknownAlbum.songs.addAll(unknownSongs)
|
||||||
|
unknownAlbum.numSongs = unknownAlbum.songs.size
|
||||||
|
|
||||||
|
for (song in unknownSongs) {
|
||||||
|
song.album = unknownAlbum
|
||||||
|
}
|
||||||
|
|
||||||
|
albums.add(unknownAlbum)
|
||||||
|
|
||||||
|
Log.d(
|
||||||
|
this::class.simpleName,
|
||||||
|
"Placed " + unknownSongs.size.toString() + " songs into an unknown album"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sortAlbumsIntoArtists() {
|
||||||
|
Log.d(this::class.simpleName, "Sorting albums into artists...")
|
||||||
|
|
||||||
|
val unknownAlbums = albums.toMutableList()
|
||||||
|
|
||||||
|
for (artist in artists) {
|
||||||
|
// Find all albums that match the current artist name
|
||||||
|
val artistAlbums = albums.filter { it.artistName == artist.name }
|
||||||
|
|
||||||
|
// And then add them to the album, along with refreshing the amount of albums
|
||||||
|
artist.albums.addAll(artistAlbums)
|
||||||
|
artist.numAlbums = artist.albums.size
|
||||||
|
|
||||||
|
unknownAlbums.removeAll(artistAlbums)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any remaining albums will be added to an unknown artist
|
||||||
|
if (unknownAlbums.size > 0) {
|
||||||
|
|
||||||
|
// Reuse an existing unknown artist if one is found
|
||||||
|
val unknownArtist = artists.find { it.name == null } ?: Artist()
|
||||||
|
|
||||||
|
unknownArtist.albums.addAll(unknownAlbums)
|
||||||
|
unknownArtist.numAlbums = albums.size
|
||||||
|
|
||||||
|
for (album in unknownAlbums) {
|
||||||
|
album.artist = unknownArtist
|
||||||
|
}
|
||||||
|
|
||||||
|
artists.add(unknownArtist)
|
||||||
|
|
||||||
|
Log.d(
|
||||||
|
this::class.simpleName,
|
||||||
|
"Placed " + unknownAlbums.size.toString() + " albums into an unknown album"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,49 +0,0 @@
|
||||||
package org.oxycblt.auxio.music
|
|
||||||
|
|
||||||
import org.oxycblt.auxio.music.models.Album
|
|
||||||
import org.oxycblt.auxio.music.models.Artist
|
|
||||||
import org.oxycblt.auxio.music.models.Song
|
|
||||||
|
|
||||||
// Sort a list of Song objects into lists of songs, albums, and artists.
|
|
||||||
fun processSongs(songs: MutableList<Song>): MutableList<Song> {
|
|
||||||
// Eliminate all duplicates from the list
|
|
||||||
// excluding the ID, as that's guaranteed to be unique [I think]
|
|
||||||
return songs.distinctBy {
|
|
||||||
it.name to it.artist to it.album to it.year to it.track to it.duration
|
|
||||||
}.toMutableList()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort a list of song objects into albums
|
|
||||||
fun sortIntoAlbums(songs: MutableList<Song>): MutableList<Album> {
|
|
||||||
val songsByAlbum = songs.groupBy { it.album }
|
|
||||||
val albumList = mutableListOf<Album>()
|
|
||||||
|
|
||||||
songsByAlbum.keys.iterator().forEach { album ->
|
|
||||||
val albumSongs = songsByAlbum[album]
|
|
||||||
albumSongs?.let {
|
|
||||||
albumList.add(
|
|
||||||
Album(albumSongs)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return albumList
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort a list of album objects into artists
|
|
||||||
fun sortIntoArtists(albums: MutableList<Album>): MutableList<Artist> {
|
|
||||||
val albumsByArtist = albums.groupBy { it.artist }
|
|
||||||
val artistList = mutableListOf<Artist>()
|
|
||||||
|
|
||||||
albumsByArtist.keys.iterator().forEach { artist ->
|
|
||||||
val artistAlbums = albumsByArtist[artist]
|
|
||||||
|
|
||||||
artistAlbums?.let {
|
|
||||||
artistList.add(
|
|
||||||
Artist(artistAlbums)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return artistList
|
|
||||||
}
|
|
|
@ -2,30 +2,13 @@ package org.oxycblt.auxio.music.models
|
||||||
|
|
||||||
// Abstraction for Song
|
// Abstraction for Song
|
||||||
data class Album(
|
data class Album(
|
||||||
var songs: List<Song>
|
val id: Long = 0L,
|
||||||
|
val title: String? = null,
|
||||||
|
val artistName: String? = null,
|
||||||
|
val year: Int = 0,
|
||||||
|
var numSongs: Int = 0
|
||||||
) {
|
) {
|
||||||
var title: String? = null
|
lateinit var artist: Artist
|
||||||
var artist: String? = null
|
|
||||||
var year: Int = 0
|
|
||||||
|
|
||||||
init {
|
val songs = mutableListOf<Song>()
|
||||||
// Iterate through the child songs and inherit the first valid value
|
|
||||||
// for the Album Name, Artist, and Year
|
|
||||||
for (song in songs) {
|
|
||||||
if (song.album != null) {
|
|
||||||
title = song.album
|
|
||||||
}
|
|
||||||
|
|
||||||
if (song.artist != null) {
|
|
||||||
artist = song.artist
|
|
||||||
}
|
|
||||||
|
|
||||||
if (song.year != 0) {
|
|
||||||
year = song.year
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also sort the songs by track
|
|
||||||
songs = songs.sortedBy { it.track }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,19 +2,10 @@ package org.oxycblt.auxio.music.models
|
||||||
|
|
||||||
// Abstraction for mAlbums
|
// Abstraction for mAlbums
|
||||||
data class Artist(
|
data class Artist(
|
||||||
private var albums: List<Album>
|
val id: Long = 0,
|
||||||
|
val name: String? = null,
|
||||||
|
val genres: MutableList<String?> = mutableListOf(null)
|
||||||
) {
|
) {
|
||||||
var name: String? = null
|
val albums = mutableListOf<Album>()
|
||||||
|
var numAlbums = 0
|
||||||
init {
|
|
||||||
// Like Album, iterate through the child albums and pick out the first valid for artist
|
|
||||||
for (album in albums) {
|
|
||||||
if (album.artist != null) {
|
|
||||||
name = album.artist
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also sort the mAlbums by year
|
|
||||||
albums = albums.sortedBy { it.year }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
package org.oxycblt.auxio.music.models
|
||||||
|
|
||||||
|
data class Genre(
|
||||||
|
val id: Long,
|
||||||
|
val name: String?
|
||||||
|
)
|
|
@ -3,12 +3,10 @@ package org.oxycblt.auxio.music.models
|
||||||
// Class containing all relevant values for a song.
|
// Class containing all relevant values for a song.
|
||||||
data class Song(
|
data class Song(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val name: String?,
|
val title: String,
|
||||||
val artist: String?,
|
val albumName: String,
|
||||||
val album: String?,
|
|
||||||
val genre: String?,
|
|
||||||
val year: Int,
|
|
||||||
val track: Int,
|
val track: Int,
|
||||||
val duration: Long,
|
val duration: Long
|
||||||
val coverData: ByteArray? = null
|
) {
|
||||||
)
|
lateinit var album: Album
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue