Break up MusicRepository into seperate files
Remove the loading/sorting functions from MusicRepository and place them into their own files.
This commit is contained in:
parent
24452e8fa4
commit
6508280900
5 changed files with 253 additions and 215 deletions
|
@ -12,7 +12,7 @@ import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentLoadingBinding
|
import org.oxycblt.auxio.databinding.FragmentLoadingBinding
|
||||||
import org.oxycblt.auxio.music.MusicLoadResponse
|
import org.oxycblt.auxio.music.MusicLoaderResponse
|
||||||
|
|
||||||
class LoadingFragment : Fragment() {
|
class LoadingFragment : Fragment() {
|
||||||
|
|
||||||
|
@ -58,12 +58,12 @@ class LoadingFragment : Fragment() {
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onMusicLoadResponse(repoResponse: MusicLoadResponse?) {
|
private fun onMusicLoadResponse(repoResponse: MusicLoaderResponse?) {
|
||||||
|
|
||||||
// Don't run this if the value is null, Which is what the value changes to after
|
// Don't run this if the value is null, Which is what the value changes to after
|
||||||
// this is run.
|
// this is run.
|
||||||
repoResponse?.let { response ->
|
repoResponse?.let { response ->
|
||||||
if (response == MusicLoadResponse.DONE) {
|
if (response == MusicLoaderResponse.DONE) {
|
||||||
this.findNavController().navigate(
|
this.findNavController().navigate(
|
||||||
LoadingFragmentDirections.actionToLibrary()
|
LoadingFragmentDirections.actionToLibrary()
|
||||||
)
|
)
|
||||||
|
@ -75,7 +75,7 @@ class LoadingFragment : Fragment() {
|
||||||
binding.statusText.visibility = View.VISIBLE
|
binding.statusText.visibility = View.VISIBLE
|
||||||
binding.resetButton.visibility = View.VISIBLE
|
binding.resetButton.visibility = View.VISIBLE
|
||||||
|
|
||||||
if (response == MusicLoadResponse.NO_MUSIC) {
|
if (response == MusicLoaderResponse.NO_MUSIC) {
|
||||||
binding.statusText.text = getString(R.string.error_no_music)
|
binding.statusText.text = getString(R.string.error_no_music)
|
||||||
} else {
|
} else {
|
||||||
binding.statusText.text = getString(R.string.error_music_load_failed)
|
binding.statusText.text = getString(R.string.error_music_load_failed)
|
||||||
|
|
|
@ -11,7 +11,7 @@ import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.oxycblt.auxio.music.MusicLoadResponse
|
import org.oxycblt.auxio.music.MusicLoaderResponse
|
||||||
import org.oxycblt.auxio.music.MusicRepository
|
import org.oxycblt.auxio.music.MusicRepository
|
||||||
|
|
||||||
class LoadingViewModel(private val app: Application) : ViewModel() {
|
class LoadingViewModel(private val app: Application) : ViewModel() {
|
||||||
|
@ -21,8 +21,8 @@ class LoadingViewModel(private val app: Application) : ViewModel() {
|
||||||
Dispatchers.IO
|
Dispatchers.IO
|
||||||
)
|
)
|
||||||
|
|
||||||
private val mMusicRepoResponse = MutableLiveData<MusicLoadResponse>()
|
private val mMusicRepoResponse = MutableLiveData<MusicLoaderResponse>()
|
||||||
val musicRepoResponse: LiveData<MusicLoadResponse> get() = mMusicRepoResponse
|
val musicRepoResponse: LiveData<MusicLoaderResponse> get() = mMusicRepoResponse
|
||||||
|
|
||||||
private val mDoRetry = MutableLiveData<Boolean>()
|
private val mDoRetry = MutableLiveData<Boolean>()
|
||||||
val doRetry: LiveData<Boolean> get() = mDoRetry
|
val doRetry: LiveData<Boolean> get() = mDoRetry
|
||||||
|
|
174
app/src/main/java/org/oxycblt/auxio/music/MusicLoader.kt
Normal file
174
app/src/main/java/org/oxycblt/auxio/music/MusicLoader.kt
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
package org.oxycblt.auxio.music
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.content.ContentUris
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.media.MediaMetadataRetriever
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.provider.MediaStore.Audio.AudioColumns
|
||||||
|
import android.util.Log
|
||||||
|
import org.oxycblt.auxio.music.models.Song
|
||||||
|
|
||||||
|
enum class MusicLoaderResponse {
|
||||||
|
DONE, FAILURE, NO_MUSIC
|
||||||
|
}
|
||||||
|
|
||||||
|
// Class that loads music from the FileSystem.
|
||||||
|
// This thing is probably full of memory leaks.
|
||||||
|
class MusicLoader(private val app: Application) {
|
||||||
|
|
||||||
|
var songs = mutableListOf<Song>()
|
||||||
|
|
||||||
|
private val retriever: MediaMetadataRetriever = MediaMetadataRetriever()
|
||||||
|
private var musicCursor: Cursor? = null
|
||||||
|
|
||||||
|
val response: MusicLoaderResponse
|
||||||
|
|
||||||
|
init {
|
||||||
|
response = findMusic()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findMusic() : MusicLoaderResponse {
|
||||||
|
try {
|
||||||
|
musicCursor = getCursor(
|
||||||
|
app.contentResolver
|
||||||
|
)
|
||||||
|
|
||||||
|
Log.i(this::class.simpleName, "Starting music search...")
|
||||||
|
|
||||||
|
useCursor()
|
||||||
|
|
||||||
|
} catch (error: Exception) {
|
||||||
|
// TODO: Add better error handling
|
||||||
|
|
||||||
|
Log.e(this::class.simpleName, "Something went horribly wrong.")
|
||||||
|
error.printStackTrace()
|
||||||
|
|
||||||
|
finalize()
|
||||||
|
|
||||||
|
return MusicLoaderResponse.FAILURE
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the main loading completed without a failure, return DONE or
|
||||||
|
// 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 getCursor(resolver: ContentResolver): Cursor? {
|
||||||
|
Log.i(this::class.simpleName, "Getting music cursor.")
|
||||||
|
|
||||||
|
return resolver.query(
|
||||||
|
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
||||||
|
arrayOf(
|
||||||
|
AudioColumns._ID,
|
||||||
|
AudioColumns.DISPLAY_NAME
|
||||||
|
),
|
||||||
|
AudioColumns.IS_MUSIC + "=1", null,
|
||||||
|
MediaStore.Audio.Media.DEFAULT_SORT_ORDER
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the cursor index music files from the shared storage, returns true if any were found.
|
||||||
|
private fun useCursor() {
|
||||||
|
musicCursor?.use { cursor ->
|
||||||
|
|
||||||
|
// Don't run the more expensive file loading operations if there is no music to index.
|
||||||
|
if (cursor.count == 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val idIndex = cursor.getColumnIndexOrThrow(AudioColumns._ID)
|
||||||
|
val displayIndex = cursor.getColumnIndexOrThrow(AudioColumns.DISPLAY_NAME)
|
||||||
|
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
val id = cursor.getLong(idIndex)
|
||||||
|
|
||||||
|
// Read the current file from the ID
|
||||||
|
retriever.setDataSource(
|
||||||
|
app.applicationContext,
|
||||||
|
ContentUris.withAppendedId(
|
||||||
|
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
||||||
|
id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get the metadata attributes
|
||||||
|
val title = retriever.extractMetadata(
|
||||||
|
MediaMetadataRetriever.METADATA_KEY_TITLE
|
||||||
|
) ?: cursor.getString(displayIndex)
|
||||||
|
|
||||||
|
val artist = retriever.extractMetadata(
|
||||||
|
MediaMetadataRetriever.METADATA_KEY_ARTIST
|
||||||
|
)
|
||||||
|
|
||||||
|
val album = retriever.extractMetadata(
|
||||||
|
MediaMetadataRetriever.METADATA_KEY_ALBUM
|
||||||
|
)
|
||||||
|
|
||||||
|
val genre = retriever.extractMetadata(
|
||||||
|
MediaMetadataRetriever.METADATA_KEY_GENRE
|
||||||
|
)
|
||||||
|
|
||||||
|
val year = (
|
||||||
|
retriever.extractMetadata(
|
||||||
|
MediaMetadataRetriever.METADATA_KEY_YEAR
|
||||||
|
) ?: "0"
|
||||||
|
).toInt()
|
||||||
|
|
||||||
|
// Track is formatted as X/0, so trim off the /0 part to parse
|
||||||
|
// the track number correctly.
|
||||||
|
val track = (
|
||||||
|
retriever.extractMetadata(
|
||||||
|
MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER
|
||||||
|
) ?: "0/0"
|
||||||
|
).split("/")[0].toInt()
|
||||||
|
|
||||||
|
// Something has gone horribly wrong if a file has no duration,
|
||||||
|
// so assert it as such.
|
||||||
|
val duration = retriever.extractMetadata(
|
||||||
|
MediaMetadataRetriever.METADATA_KEY_DURATION
|
||||||
|
)!!.toLong()
|
||||||
|
|
||||||
|
// TODO: Add int-based genre compatibility
|
||||||
|
songs.add(
|
||||||
|
Song(
|
||||||
|
title,
|
||||||
|
artist,
|
||||||
|
album,
|
||||||
|
genre,
|
||||||
|
year,
|
||||||
|
track,
|
||||||
|
duration,
|
||||||
|
|
||||||
|
retriever.embeddedPicture,
|
||||||
|
id
|
||||||
|
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Free the metadata retriever & the musicCursor, and make the song list immutable.
|
||||||
|
private fun finalize() {
|
||||||
|
retriever.close()
|
||||||
|
musicCursor?.use{
|
||||||
|
it.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,227 +1,40 @@
|
||||||
package org.oxycblt.auxio.music
|
package org.oxycblt.auxio.music
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.ContentResolver
|
|
||||||
import android.content.ContentUris
|
|
||||||
import android.database.Cursor
|
|
||||||
import android.media.MediaMetadataRetriever
|
|
||||||
import android.provider.MediaStore
|
|
||||||
import android.provider.MediaStore.Audio.AudioColumns
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.oxycblt.auxio.music.models.Album
|
import org.oxycblt.auxio.music.models.Album
|
||||||
import org.oxycblt.auxio.music.models.Artist
|
import org.oxycblt.auxio.music.models.Artist
|
||||||
import org.oxycblt.auxio.music.models.Song
|
import org.oxycblt.auxio.music.models.Song
|
||||||
|
|
||||||
enum class MusicLoadResponse {
|
// Storage for music data.
|
||||||
DONE, FAILURE, NO_MUSIC
|
|
||||||
}
|
|
||||||
|
|
||||||
// Storage for music data. Design largely adapted from Music Player GO:
|
|
||||||
// https://github.com/enricocid/Music-Player-GO
|
|
||||||
class MusicRepository {
|
class MusicRepository {
|
||||||
|
|
||||||
private lateinit var mArtists: List<Artist>
|
private val mArtists = MutableLiveData<List<Artist>>()
|
||||||
private lateinit var mAlbums: List<Album>
|
private var mAlbums = MutableLiveData<List<Album>>()
|
||||||
private lateinit var mSongs: List<Song>
|
private var mSongs = MutableLiveData<List<Song>>()
|
||||||
|
|
||||||
// Not sure if backings are necessary but they're vars so better safe than sorry
|
val artists: LiveData<List<Artist>> get() = mArtists
|
||||||
val artists: List<Artist> get() = mArtists
|
val albums: LiveData<List<Album>> get() = mAlbums
|
||||||
val albums: List<Album> get() = mAlbums
|
val songs: LiveData<List<Song>> get() = mSongs
|
||||||
val songs: List<Song> get() = mSongs
|
|
||||||
|
|
||||||
fun init(app: Application): MusicLoadResponse {
|
suspend fun init(app: Application): MusicLoaderResponse {
|
||||||
|
val loader = MusicLoader(app)
|
||||||
|
|
||||||
findMusic(app)?.let { ss ->
|
if (loader.response == MusicLoaderResponse.DONE) {
|
||||||
return if (ss.size > 0) {
|
// If the loading succeeds, then process the songs into lists of
|
||||||
processSongs(ss)
|
// songs, albums, and artists on the main thread.
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
MusicLoadResponse.DONE
|
mSongs.value = processSongs(loader.songs)
|
||||||
} else {
|
mAlbums.value = sortIntoAlbums(mSongs.value as MutableList<Song>)
|
||||||
MusicLoadResponse.NO_MUSIC
|
mArtists.value = sortIntoArtists(mAlbums.value as MutableList<Album>)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the let function isn't run, then the loading has failed due to some Exception
|
return loader.response
|
||||||
// and FAILURE must be returned
|
|
||||||
return MusicLoadResponse.FAILURE
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun findMusic(app: Application): MutableList<Song>? {
|
|
||||||
|
|
||||||
val songList = mutableListOf<Song>()
|
|
||||||
val retriever = MediaMetadataRetriever()
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
val musicCursor = getCursor(
|
|
||||||
app.contentResolver
|
|
||||||
)
|
|
||||||
|
|
||||||
Log.i(this::class.simpleName, "Starting music search...")
|
|
||||||
|
|
||||||
// Index music files from shared storage
|
|
||||||
musicCursor?.use { cursor ->
|
|
||||||
|
|
||||||
// Don't run the more expensive file loading operations if there is no music
|
|
||||||
// to index.
|
|
||||||
if (cursor.count == 0) {
|
|
||||||
return songList
|
|
||||||
}
|
|
||||||
|
|
||||||
val idIndex = cursor.getColumnIndexOrThrow(AudioColumns._ID)
|
|
||||||
val displayIndex = cursor.getColumnIndexOrThrow(AudioColumns.DISPLAY_NAME)
|
|
||||||
|
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
val id = cursor.getLong(idIndex)
|
|
||||||
|
|
||||||
// Read the current file from the ID
|
|
||||||
retriever.setDataSource(
|
|
||||||
app.applicationContext,
|
|
||||||
ContentUris.withAppendedId(
|
|
||||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
|
||||||
id
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Get the metadata attributes
|
|
||||||
val title = retriever.extractMetadata(
|
|
||||||
MediaMetadataRetriever.METADATA_KEY_TITLE
|
|
||||||
) ?: cursor.getString(displayIndex)
|
|
||||||
|
|
||||||
val artist = retriever.extractMetadata(
|
|
||||||
MediaMetadataRetriever.METADATA_KEY_ARTIST
|
|
||||||
)
|
|
||||||
|
|
||||||
val album = retriever.extractMetadata(
|
|
||||||
MediaMetadataRetriever.METADATA_KEY_ALBUM
|
|
||||||
)
|
|
||||||
|
|
||||||
val genre = retriever.extractMetadata(
|
|
||||||
MediaMetadataRetriever.METADATA_KEY_GENRE
|
|
||||||
)
|
|
||||||
|
|
||||||
val year = (
|
|
||||||
retriever.extractMetadata(
|
|
||||||
MediaMetadataRetriever.METADATA_KEY_YEAR
|
|
||||||
) ?: "0"
|
|
||||||
).toInt()
|
|
||||||
|
|
||||||
// Track is formatted as X/0, so trim off the /0 part to parse
|
|
||||||
// the track number correctly.
|
|
||||||
val track = (
|
|
||||||
retriever.extractMetadata(
|
|
||||||
MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER
|
|
||||||
) ?: "0/0"
|
|
||||||
).split("/")[0].toInt()
|
|
||||||
|
|
||||||
// Something has gone horribly wrong if a file has no duration,
|
|
||||||
// so assert it as such.
|
|
||||||
val duration = retriever.extractMetadata(
|
|
||||||
MediaMetadataRetriever.METADATA_KEY_DURATION
|
|
||||||
)!!.toLong()
|
|
||||||
|
|
||||||
// TODO: Add int-based genre compatibility
|
|
||||||
songList.add(
|
|
||||||
Song(
|
|
||||||
title,
|
|
||||||
artist,
|
|
||||||
album,
|
|
||||||
genre,
|
|
||||||
year,
|
|
||||||
track,
|
|
||||||
duration,
|
|
||||||
|
|
||||||
retriever.embeddedPicture,
|
|
||||||
id
|
|
||||||
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close the retriever/cursor so that it gets garbage collected
|
|
||||||
retriever.close()
|
|
||||||
cursor.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.d(
|
|
||||||
this::class.simpleName,
|
|
||||||
"Successfully found " + songList.size.toString() + " Songs."
|
|
||||||
)
|
|
||||||
|
|
||||||
return songList
|
|
||||||
} catch (error: Exception) {
|
|
||||||
// TODO: Add better error handling
|
|
||||||
|
|
||||||
Log.e(this::class.simpleName, "Something went horribly wrong.")
|
|
||||||
error.printStackTrace()
|
|
||||||
|
|
||||||
retriever.close()
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getCursor(resolver: ContentResolver): Cursor? {
|
|
||||||
Log.i(this::class.simpleName, "Getting music cursor.")
|
|
||||||
|
|
||||||
return resolver.query(
|
|
||||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
|
||||||
arrayOf(
|
|
||||||
AudioColumns._ID,
|
|
||||||
AudioColumns.DISPLAY_NAME
|
|
||||||
),
|
|
||||||
AudioColumns.IS_MUSIC + "=1", null,
|
|
||||||
MediaStore.Audio.Media.DEFAULT_SORT_ORDER
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort the list of Song objects into an abstracted lis
|
|
||||||
private fun processSongs(songs: MutableList<Song>) {
|
|
||||||
// Eliminate all duplicates from the list
|
|
||||||
// excluding the ID, as that's guaranteed to be unique [I think]
|
|
||||||
val distinctSongs = songs.distinctBy {
|
|
||||||
it.name to it.artist to it.album to it.year to it.track to it.duration
|
|
||||||
}.toMutableList()
|
|
||||||
|
|
||||||
// Add an album abstraction for each group of songs
|
|
||||||
val songsByAlbum = distinctSongs.groupBy { it.album }
|
|
||||||
val albumList = mutableListOf<Album>()
|
|
||||||
|
|
||||||
songsByAlbum.keys.iterator().forEach { album ->
|
|
||||||
val albumSongs = songsByAlbum[album]
|
|
||||||
albumSongs?.let {
|
|
||||||
albumList.add(
|
|
||||||
Album(albumSongs)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then abstract the remaining albums into artist objects
|
|
||||||
val albumsByArtist = albumList.groupBy { it.artist }
|
|
||||||
val artistList = mutableListOf<Artist>()
|
|
||||||
|
|
||||||
albumsByArtist.keys.iterator().forEach { artist ->
|
|
||||||
val artistAlbums = albumsByArtist[artist]
|
|
||||||
|
|
||||||
artistAlbums?.let {
|
|
||||||
artistList.add(
|
|
||||||
Artist(artistAlbums)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.i(
|
|
||||||
this::class.simpleName,
|
|
||||||
"Successfully sorted songs into " +
|
|
||||||
artistList.size.toString() +
|
|
||||||
" Artists and " +
|
|
||||||
albumList.size.toString() +
|
|
||||||
" Albums."
|
|
||||||
)
|
|
||||||
|
|
||||||
mArtists = artistList
|
|
||||||
mAlbums = albumList
|
|
||||||
mSongs = distinctSongs
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
51
app/src/main/java/org/oxycblt/auxio/music/MusicSorting.kt
Normal file
51
app/src/main/java/org/oxycblt/auxio/music/MusicSorting.kt
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
Loading…
Reference in a new issue