From 6e5bff3bd32b597c01a971603a922c85323e9fb4 Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Sun, 20 Dec 2020 15:30:50 -0700 Subject: [PATCH] Heavily refactor music loading Heavily change how music loading works so that artists arent redundantly loaded, genres are primarily song-based instead of genre based, and songs are now bound to their correct genres. --- .../oxycblt/auxio/detail/DetailFragment.kt | 1 + .../auxio/detail/GenreDetailFragment.kt | 30 ++-- .../detail/adapters/GenreArtistAdapter.kt | 41 ----- .../auxio/detail/adapters/GenreSongAdapter.kt | 25 +++ .../oxycblt/auxio/loading/LoadingFragment.kt | 6 +- .../oxycblt/auxio/loading/LoadingViewModel.kt | 6 +- .../java/org/oxycblt/auxio/music/Models.kt | 112 +++++++++--- .../org/oxycblt/auxio/music/MusicStore.kt | 37 +--- .../org/oxycblt/auxio/music/MusicUtils.kt | 27 ++- .../org/oxycblt/auxio/music/coil/CoilUtils.kt | 26 +-- .../auxio/music/processing/MusicLoader.kt | 157 ++++++----------- .../auxio/music/processing/MusicSorter.kt | 162 ++---------------- .../playback/state/PlaybackStateManager.kt | 77 ++------- .../res/layout-land/fragment_album_detail.xml | 2 +- .../layout-land/fragment_artist_detail.xml | 2 +- .../res/layout-land/fragment_genre_detail.xml | 18 +- .../main/res/layout/fragment_album_detail.xml | 2 +- .../res/layout/fragment_artist_detail.xml | 2 +- .../main/res/layout/fragment_genre_detail.xml | 15 +- app/src/main/res/layout/item_genre.xml | 4 +- 20 files changed, 264 insertions(+), 488 deletions(-) delete mode 100644 app/src/main/java/org/oxycblt/auxio/detail/adapters/GenreArtistAdapter.kt create mode 100644 app/src/main/java/org/oxycblt/auxio/detail/adapters/GenreSongAdapter.kt diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt index 19827e3f8..ce1f70570 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt @@ -12,6 +12,7 @@ import androidx.navigation.fragment.findNavController * instead of out of the app if a Detail Fragment is currently open. Also carries the * multi-navigation fix. * TODO: Migrate to a more powerful/efficient CoordinatorLayout instead of NestedScrollView + * TODO: Add custom artist images * @author OxygenCobalt */ abstract class DetailFragment : Fragment() { diff --git a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt index db3ab6518..591355e21 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -10,12 +10,13 @@ import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentGenreDetailBinding -import org.oxycblt.auxio.detail.adapters.GenreArtistAdapter +import org.oxycblt.auxio.detail.adapters.GenreSongAdapter import org.oxycblt.auxio.logD import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.playback.PlaybackViewModel +import org.oxycblt.auxio.playback.state.PlaybackMode import org.oxycblt.auxio.ui.disable -import org.oxycblt.auxio.ui.setupArtistActions +import org.oxycblt.auxio.ui.setupSongActions /** * The [DetailFragment] for a genre. @@ -45,19 +46,13 @@ class GenreDetailFragment : DetailFragment() { ) } - val artistAdapter = GenreArtistAdapter( + val songAdapter = GenreSongAdapter( doOnClick = { - if (!detailModel.isNavigating) { - detailModel.updateNavigationStatus(true) - - findNavController().navigate( - GenreDetailFragmentDirections.actionShowArtist(it.id) - ) - } + playbackModel.playSong(it, PlaybackMode.IN_GENRE) }, doOnLongClick = { data, view -> - PopupMenu(requireContext(), view).setupArtistActions( - data, playbackModel + PopupMenu(requireContext(), view).setupSongActions( + requireContext(), data, playbackModel ) } ) @@ -96,13 +91,12 @@ class GenreDetailFragment : DetailFragment() { } } - // Disable the sort button if there is only one artist [Or less] - if (detailModel.currentGenre.value!!.artists.size < 2) { + if (detailModel.currentGenre.value!!.songs.size < 2) { binding.genreSortButton.disable(requireContext()) } - binding.genreArtistRecycler.apply { - adapter = artistAdapter + binding.genreSongRecycler.apply { + adapter = songAdapter setHasFixedSize(true) } @@ -115,8 +109,8 @@ class GenreDetailFragment : DetailFragment() { binding.genreSortButton.setImageResource(mode.iconRes) // Then update the sort mode of the artist adapter. - artistAdapter.submitList( - mode.getSortedArtistList(detailModel.currentGenre.value!!.artists) + songAdapter.submitList( + mode.getSortedSongList(detailModel.currentGenre.value!!.songs) ) } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/adapters/GenreArtistAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/adapters/GenreArtistAdapter.kt deleted file mode 100644 index 188f1e867..000000000 --- a/app/src/main/java/org/oxycblt/auxio/detail/adapters/GenreArtistAdapter.kt +++ /dev/null @@ -1,41 +0,0 @@ -package org.oxycblt.auxio.detail.adapters - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.recyclerview.widget.ListAdapter -import org.oxycblt.auxio.databinding.ItemGenreArtistBinding -import org.oxycblt.auxio.music.Artist -import org.oxycblt.auxio.recycler.DiffCallback -import org.oxycblt.auxio.recycler.viewholders.BaseViewHolder - -/** - * An adapter for displaying the [Artist]s of an genre. - */ -class GenreArtistAdapter( - private val doOnClick: (data: Artist) -> Unit, - private val doOnLongClick: (data: Artist, view: View) -> Unit -) : ListAdapter(DiffCallback()) { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - return ViewHolder( - ItemGenreArtistBinding.inflate(LayoutInflater.from(parent.context)) - ) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - holder.bind(getItem(position)) - } - - // Generic ViewHolder for an album - inner class ViewHolder( - private val binding: ItemGenreArtistBinding - ) : BaseViewHolder(binding, doOnClick, doOnLongClick) { - - override fun onBind(data: Artist) { - binding.artist = data - - binding.artistName.requestLayout() - } - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/detail/adapters/GenreSongAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/adapters/GenreSongAdapter.kt new file mode 100644 index 000000000..8a7c64331 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/detail/adapters/GenreSongAdapter.kt @@ -0,0 +1,25 @@ +package org.oxycblt.auxio.detail.adapters + +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.ListAdapter +import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.recycler.DiffCallback +import org.oxycblt.auxio.recycler.viewholders.SongViewHolder + +/** + * An adapter for displaying the [Song]s of a genre. + */ +class GenreSongAdapter( + private val doOnClick: (data: Song) -> Unit, + private val doOnLongClick: (data: Song, view: View) -> Unit +) : ListAdapter(DiffCallback()) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SongViewHolder { + return SongViewHolder.from(parent.context, doOnClick, doOnLongClick) + } + + override fun onBindViewHolder(holder: SongViewHolder, position: Int) { + holder.bind(getItem(position)) + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/loading/LoadingFragment.kt b/app/src/main/java/org/oxycblt/auxio/loading/LoadingFragment.kt index e1453fd3d..1389a0d2c 100644 --- a/app/src/main/java/org/oxycblt/auxio/loading/LoadingFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/loading/LoadingFragment.kt @@ -15,7 +15,7 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentLoadingBinding import org.oxycblt.auxio.logD import org.oxycblt.auxio.music.MusicStore -import org.oxycblt.auxio.music.processing.MusicLoaderResponse +import org.oxycblt.auxio.music.processing.MusicLoader /** * An intermediary [Fragment] that asks for the READ_EXTERNAL_STORAGE permission and runs @@ -69,7 +69,7 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) { // --- VIEWMODEL SETUP --- loadingModel.response.observe(viewLifecycleOwner) { - if (it == MusicLoaderResponse.DONE) { + if (it == MusicLoader.Response.SUCCESS) { findNavController().navigate( LoadingFragmentDirections.actionToMain() ) @@ -77,7 +77,7 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) { // If the response wasn't a success, then show the specific error message // depending on which error response was given, along with a retry button binding.loadingErrorText.text = - if (it == MusicLoaderResponse.NO_MUSIC) + if (it == MusicLoader.Response.NO_MUSIC) getString(R.string.error_no_music) else getString(R.string.error_music_load_failed) diff --git a/app/src/main/java/org/oxycblt/auxio/loading/LoadingViewModel.kt b/app/src/main/java/org/oxycblt/auxio/loading/LoadingViewModel.kt index d655a6d20..ce7eaff33 100644 --- a/app/src/main/java/org/oxycblt/auxio/loading/LoadingViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/loading/LoadingViewModel.kt @@ -10,7 +10,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.oxycblt.auxio.music.MusicStore -import org.oxycblt.auxio.music.processing.MusicLoaderResponse +import org.oxycblt.auxio.music.processing.MusicLoader /** * A [ViewModel] responsible for getting the music loading process going and managing the response @@ -18,8 +18,8 @@ import org.oxycblt.auxio.music.processing.MusicLoaderResponse * @author OxygenCobalt */ class LoadingViewModel(private val app: Application) : ViewModel() { - private val mResponse = MutableLiveData() - val response: LiveData get() = mResponse + private val mResponse = MutableLiveData() + val response: LiveData get() = mResponse private val mRedo = MutableLiveData() val doReload: LiveData get() = mRedo diff --git a/app/src/main/java/org/oxycblt/auxio/music/Models.kt b/app/src/main/java/org/oxycblt/auxio/music/Models.kt index 704837b9d..213fb6210 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Models.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Models.kt @@ -21,6 +21,7 @@ sealed class BaseModel { * @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] * @property seconds The Song's duration in seconds * @property formattedDuration The Song's duration as a duration string. * @author OxygenCobalt @@ -32,7 +33,31 @@ data class Song( val track: Int = -1, val duration: Long = 0, ) : BaseModel() { - lateinit var album: Album + private var mAlbum: Album? = null + private var mGenre: Genre? = null + + val genre: Genre? get() = mGenre + val album: Album get() { + val album = mAlbum + + if (album != null) { + return album + } else { + error("Song $name must have an album") + } + } + + fun applyGenre(genre: Genre) { + check(mGenre == null) { "Genre is already applied" } + + mGenre = genre + } + + fun applyAlbum(album: Album) { + check(mAlbum == null) { "Album is already applied" } + + mAlbum = album + } val seconds = duration / 1000 val formattedDuration: String = seconds.toDuration() @@ -40,24 +65,35 @@ data class Song( /** * The data object for an album. Inherits [BaseModel]. - * @property artistId The Album's parent artist ID. Do not use this outside of attaching an album to its parent artist. - * @property coverUri The [Uri] for the album's cover. **Load this using Coil.** - * @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 [artistId] - * @property songs The Album's child [Song]s. + * @property artistName The name of the parent artist. Do not use this outside of creating the artist from albums + * @property coverUri The [Uri] for the album's cover. **Load this using Coil.** + * @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. * @author OxygenCobalt */ data class Album( override val id: Long = -1, override val name: String, - val artistId: Long = -1, + val artistName: String, val coverUri: Uri = Uri.EMPTY, val year: Int = 0 ) : BaseModel() { - lateinit var artist: Artist + private var mArtist: Artist? = null + val artist: Artist get() { + val artist = mArtist + + if (artist != null) { + return artist + } else { + error("Album $name must have an artist") + } + } + + private val mSongs = mutableListOf() + val songs: List get() = mSongs - val songs = mutableListOf() val totalDuration: String by lazy { var seconds: Long = 0 songs.forEach { @@ -65,21 +101,44 @@ data class Album( } seconds.toDuration() } + + fun applySongs(songs: List) { + songs.forEach { + it.applyAlbum(this) + mSongs.add(it) + } + } + + fun applyArtist(artist: Artist) { + mArtist = artist + } } /** * The data object for an artist. Inherits [BaseModel] * @property albums The list of all [Album]s in this artist - * @property genres The list of all parent [Genre]s in this artist, sorted by relevance + * @property genre The most prominent genre for this artist * @property songs The list of all [Song]s in this artist * @author OxygenCobalt */ data class Artist( override val id: Long = -1, - override var name: String + override var name: String, + val albums: List ) : BaseModel() { - val albums = mutableListOf() - val genres = mutableListOf() + init { + albums.forEach { + it.applyArtist(this) + } + } + + val genre: Genre? by lazy { + val groupedGenres = songs.groupBy { it.genre } + + groupedGenres.keys.maxByOrNull { key -> + groupedGenres[key]?.size ?: 0 + } + } val songs: List by lazy { val songs = mutableListOf() @@ -92,8 +151,6 @@ data class Artist( /** * The data object for a genre. Inherits [BaseModel] - * @property artists The list of all [Artist]s in this genre. - * @property albums The list of all [Album]s in this genre. * @property songs The list of all [Song]s in this genre. * @author OxygenCobalt */ @@ -101,21 +158,20 @@ data class Genre( override val id: Long = -1, override var name: String, ) : BaseModel() { - val artists = mutableListOf() + private val mSongs = mutableListOf() + val songs: List get() = mSongs - val albums: List by lazy { - val albums = mutableListOf() - artists.forEach { - albums.addAll(it.albums) - } - albums + val albumCount: Int by lazy { + songs.groupBy { it.album }.size } - val songs: List by lazy { - val songs = mutableListOf() - artists.forEach { - songs.addAll(it.songs) - } - songs + + val artistCount: Int by lazy { + songs.groupBy { it.album.artist }.size + } + + fun addSong(song: Song) { + mSongs.add(song) + song.applyGenre(this) } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt index c5670a3f2..34c83c472 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt @@ -3,16 +3,13 @@ package org.oxycblt.auxio.music import android.app.Application import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.oxycblt.auxio.R import org.oxycblt.auxio.logD import org.oxycblt.auxio.music.processing.MusicLoader -import org.oxycblt.auxio.music.processing.MusicLoaderResponse import org.oxycblt.auxio.music.processing.MusicSorter import org.oxycblt.auxio.recycler.DisplayMode /** * The main storage for music items. Use [MusicStore.getInstance] to get the single instance of it. - * TODO: Completely rewrite this system. * @author OxygenCobalt */ class MusicStore private constructor() { @@ -45,43 +42,25 @@ class MusicStore private constructor() { * ***THIS SHOULD ONLY BE RAN FROM AN IO THREAD.*** * @param app [Application] required to load the music. */ - suspend fun load(app: Application): MusicLoaderResponse { + suspend fun load(app: Application): MusicLoader.Response { return withContext(Dispatchers.IO) { this@MusicStore.logD("Starting initial music load...") val start = System.currentTimeMillis() - // 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) + val response = loader.loadMusic() - val loader = MusicLoader( - app.contentResolver, - - genrePlaceholder, - artistPlaceholder, - albumPlaceholder - ) - - if (loader.response == MusicLoaderResponse.DONE) { + if (response == MusicLoader.Response.SUCCESS) { // If the loading succeeds, then sort the songs and update the value - val sorter = MusicSorter( - loader.genres, - loader.artists, - loader.albums, - loader.songs, + val sorter = MusicSorter(loader.songs, loader.albums) - genrePlaceholder, - artistPlaceholder, - albumPlaceholder - ) + sorter.sort() mSongs = sorter.songs.toList() mAlbums = sorter.albums.toList() mArtists = sorter.artists.toList() - mGenres = sorter.genres.toList() + mGenres = loader.genres.toList() val elapsed = System.currentTimeMillis() - start @@ -90,7 +69,7 @@ class MusicStore private constructor() { loaded = true } - loader.response + response } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicUtils.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicUtils.kt index 71de561ac..ed52f1b92 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicUtils.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicUtils.kt @@ -111,26 +111,37 @@ fun Int.toYear(context: Context): String { @BindingAdapter("genreCounts") fun TextView.bindGenreCounts(genre: Genre) { val artists = context.resources.getQuantityString( - R.plurals.format_artist_count, genre.artists.size, genre.artists.size + R.plurals.format_artist_count, genre.artistCount, genre.artistCount ) val albums = context.resources.getQuantityString( - R.plurals.format_album_count, genre.albums.size, genre.albums.size + R.plurals.format_album_count, genre.albumCount, genre.albumCount ) text = context.getString(R.string.format_double_counts, artists, albums) } +/** + * Bind the album + song counts for a genre + */ +@BindingAdapter("altGenreCounts") +fun TextView.bindAltGenreCounts(genre: Genre) { + val albums = context.resources.getQuantityString( + R.plurals.format_album_count, genre.albumCount, genre.albumCount + ) + + val songs = context.resources.getQuantityString( + R.plurals.format_song_count, genre.songs.size, genre.songs.size + ) + + text = context.getString(R.string.format_double_counts, albums, songs) +} + /** * Bind the most prominent artist genre */ -// TODO: Add option to list all genres @BindingAdapter("artistGenre") fun TextView.bindArtistGenre(artist: Artist) { - text = if (artist.genres.isNotEmpty()) { - artist.genres[0].name - } else { - context.getString(R.string.placeholder_genre) - } + text = artist.genre?.name ?: context.getString(R.string.placeholder_genre) } /** diff --git a/app/src/main/java/org/oxycblt/auxio/music/coil/CoilUtils.kt b/app/src/main/java/org/oxycblt/auxio/music/coil/CoilUtils.kt index 6eb44167d..bb46bb3c0 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/coil/CoilUtils.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/coil/CoilUtils.kt @@ -104,34 +104,24 @@ fun ImageView.bindArtistImage(artist: Artist) { @BindingAdapter("genreImage") fun ImageView.bindGenreImage(genre: Genre) { val request: ImageRequest + val genreCovers = mutableListOf() - if (genre.artists.size >= 4) { - val uris = mutableListOf() - - // Get the Nth cover from each artist, if possible. - for (i in 0..3) { - val artist = genre.artists[i] - - uris.add( - if (artist.albums.size > i) { - artist.albums[i].coverUri - } else { - artist.albums[0].coverUri - } - ) - } + genre.songs.groupBy { it.album }.forEach { + genreCovers.add(it.key.coverUri) + } + if (genreCovers.size >= 4) { val fetcher = MosaicFetcher(context) request = getDefaultRequest(context, this) - .data(uris) + .data(genreCovers.slice(0..3)) .fetcher(fetcher) .error(R.drawable.ic_genre) .build() } else { - if (genre.artists.isNotEmpty()) { + if (genreCovers.isNotEmpty()) { request = getDefaultRequest(context, this) - .data(genre.artists[0].albums[0].coverUri) + .data(genreCovers[0]) .error(R.drawable.ic_genre) .build() } else { diff --git a/app/src/main/java/org/oxycblt/auxio/music/processing/MusicLoader.kt b/app/src/main/java/org/oxycblt/auxio/music/processing/MusicLoader.kt index 28723a7db..3fe7c78e2 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/processing/MusicLoader.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/processing/MusicLoader.kt @@ -1,69 +1,47 @@ package org.oxycblt.auxio.music.processing import android.annotation.SuppressLint -import android.content.ContentResolver -import android.provider.MediaStore +import android.app.Application import android.provider.MediaStore.Audio.Albums -import android.provider.MediaStore.Audio.Artists import android.provider.MediaStore.Audio.Genres import android.provider.MediaStore.Audio.Media +import androidx.core.database.getStringOrNull +import org.oxycblt.auxio.R import org.oxycblt.auxio.logD import org.oxycblt.auxio.logE import org.oxycblt.auxio.music.Album -import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.toAlbumArtURI import org.oxycblt.auxio.music.toNamedGenre -/** The response that [MusicLoader] gives when the process is done */ -enum class MusicLoaderResponse { - DONE, FAILURE, NO_MUSIC -} - /** * Object that loads music from the filesystem. - * TODO: Add custom artist images from the filesystem - * TODO: Move genre loading to songs [Loads would take longer though] */ -class MusicLoader( - private val resolver: ContentResolver, - - private val genrePlaceholder: String, - private val artistPlaceholder: String, - private val albumPlaceholder: String, -) { +class MusicLoader(private val app: Application) { var genres = mutableListOf() - var artists = mutableListOf() var albums = mutableListOf() var songs = mutableListOf() - val response: MusicLoaderResponse + private val resolver = app.contentResolver - init { - response = findMusic() - } - - private fun findMusic(): MusicLoaderResponse { + fun loadMusic(): Response { try { - val start = System.currentTimeMillis() loadGenres() - loadArtists() loadAlbums() loadSongs() - logD("Done in ${System.currentTimeMillis() - start}ms") } catch (error: Exception) { logE("Something went horribly wrong.") error.printStackTrace() - return MusicLoaderResponse.FAILURE + return Response.FAILED } if (songs.size == 0) { - return MusicLoaderResponse.NO_MUSIC + return Response.NO_MUSIC } - return MusicLoaderResponse.DONE + return Response.SUCCESS } private fun loadGenres() { @@ -80,6 +58,8 @@ class MusicLoader( Genres.DEFAULT_SORT_ORDER ) + val genrePlaceholder = app.getString(R.string.placeholder_genre) + // And then process those into Genre objects genreCursor?.use { cursor -> val idIndex = cursor.getColumnIndexOrThrow(Genres._ID) @@ -87,9 +67,9 @@ class MusicLoader( while (cursor.moveToNext()) { val id = cursor.getLong(idIndex) - var name = cursor.getString(nameIndex) ?: genrePlaceholder + var name = cursor.getStringOrNull(nameIndex) ?: genrePlaceholder - // If a genre is still in an old int-based format [Android formats it as "(INT)"],mu + // 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(Regex("[0123456789)]"))) { name = name.toNamedGenre() ?: genrePlaceholder @@ -108,73 +88,6 @@ class MusicLoader( logD("Genre search finished with ${genres.size} genres found.") } - private fun loadArtists() { - logD("Starting artist search...") - - // Load all the artists - val artistCursor = resolver.query( - Artists.EXTERNAL_CONTENT_URI, - arrayOf( - Artists._ID, // 0 - Artists.ARTIST // 1 - ), - null, null, - Artists.DEFAULT_SORT_ORDER - ) - - artistCursor?.use { cursor -> - val idIndex = cursor.getColumnIndexOrThrow(Artists._ID) - val nameIndex = cursor.getColumnIndexOrThrow(Artists.ARTIST) - - while (cursor.moveToNext()) { - val id = cursor.getLong(idIndex) - var name = cursor.getString(nameIndex) - - if (name == null || name == MediaStore.UNKNOWN_STRING) { - name = artistPlaceholder - } - - artists.add( - Artist( - id, name - ) - ) - } - - cursor.close() - } - - artists = artists.distinctBy { - it.name to it.genres - }.toMutableList() - - // Then try to associate any genres with their respective artists. - for (genre in genres) { - val artistGenreCursor = resolver.query( - Genres.Members.getContentUri("external", genre.id), - arrayOf( - Genres.Members.ARTIST_ID - ), - null, null, null - ) - - artistGenreCursor?.let { cursor -> - val idIndex = cursor.getColumnIndexOrThrow(Genres.Members.ARTIST_ID) - while (cursor.moveToNext()) { - val id = cursor.getLong(idIndex) - - artists.filter { it.id == id }.forEach { - it.genres.add(genre) - } - } - } - - artistGenreCursor?.close() - } - - logD("Artist search finished with ${artists.size} artists found.") - } - @SuppressLint("InlinedApi") private fun loadAlbums() { logD("Starting album search...") @@ -184,7 +97,7 @@ class MusicLoader( arrayOf( Albums._ID, // 0 Albums.ALBUM, // 1 - Albums.ARTIST_ID, // 2 + Albums.ARTIST, // 2 Albums.FIRST_YEAR, // 3 ), @@ -192,24 +105,26 @@ class MusicLoader( Albums.DEFAULT_SORT_ORDER ) + val albumPlaceholder = app.getString(R.string.placeholder_album) + val artistPlaceholder = app.getString(R.string.placeholder_artist) + albumCursor?.use { cursor -> val idIndex = cursor.getColumnIndexOrThrow(Albums._ID) val nameIndex = cursor.getColumnIndexOrThrow(Albums.ALBUM) - val artistIdIndex = cursor.getColumnIndexOrThrow(Albums.ARTIST_ID) + val artistIdIndex = cursor.getColumnIndexOrThrow(Albums.ARTIST) val yearIndex = cursor.getColumnIndexOrThrow(Albums.FIRST_YEAR) while (cursor.moveToNext()) { val id = cursor.getLong(idIndex) val name = cursor.getString(nameIndex) ?: albumPlaceholder - val artistId = cursor.getLong(artistIdIndex) + val artistName = cursor.getString(artistIdIndex) ?: artistPlaceholder val year = cursor.getInt(yearIndex) - val coverUri = id.toAlbumArtURI() albums.add( Album( - id, name, artistId, - coverUri, year + id = id, name = name, artistName = artistName, + coverUri = coverUri, year = year ) ) } @@ -218,7 +133,7 @@ class MusicLoader( } albums = albums.distinctBy { - it.name to it.artistId to it.year + it.name to it.artistName to it.year }.toMutableList() logD("Album search finished with ${albums.size} albums found") @@ -272,6 +187,34 @@ class MusicLoader( it.name to it.albumId to it.track to it.duration }.toMutableList() + // Then try to associate any genres with their respective songs + // This is stupidly inefficient, but I don't have another choice really. + for (genre in genres) { + val songGenreCursor = resolver.query( + Genres.Members.getContentUri("external", genre.id), + arrayOf( + Genres.Members._ID + ), + null, null, null + ) + + songGenreCursor?.use { cursor -> + val idIndex = cursor.getColumnIndexOrThrow(Genres.Members._ID) + + while (cursor.moveToNext()) { + val id = cursor.getLong(idIndex) + + songs.find { it.id == id }?.let { + genre.addSong(it) + } + } + } + } + logD("Song search finished with ${songs.size} found") } + + enum class Response { + SUCCESS, FAILED, NO_MUSIC + } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/processing/MusicSorter.kt b/app/src/main/java/org/oxycblt/auxio/music/processing/MusicSorter.kt index 438f8e3b4..51dacf033 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/processing/MusicSorter.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/processing/MusicSorter.kt @@ -3,160 +3,34 @@ package org.oxycblt.auxio.music.processing import org.oxycblt.auxio.logD import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist -import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Song -/** - * The sorter object for music loading. - */ class MusicSorter( - var genres: MutableList, - val artists: MutableList, - val albums: MutableList, val songs: MutableList, - - private val genrePlaceholder: String, - private val artistPlaceholder: String, - private val albumPlaceholder: String, + val albums: MutableList ) { - init { - sortSongsIntoAlbums() - sortAlbumsIntoArtists() - sortArtistsIntoGenres() - fixBuggyGenres() - } + val artists = mutableListOf() - private fun sortSongsIntoAlbums() { - logD("Sorting songs into albums...") - - val unknownSongs = songs.toMutableList() - - for (album in albums) { - // Find all songs that match the current album ID to prevent any bugs w/comparing names. - // This cant be done anywhere else sadly. Blame the genre system. - val albumSongs = songs.filter { it.albumId == album.id } - - // Then add them to the album - for (song in albumSongs) { - song.album = album - album.songs.add(song) - } - - unknownSongs.removeAll(albumSongs) + fun sort() { + albums.forEach { + groupSongsIntoAlbum(it) } - // Any remaining songs will be added to an unknown album - if (unknownSongs.size > 0) { + createArtistsFromAlbums(albums) + } - val unknownAlbum = Album( - name = albumPlaceholder + private fun groupSongsIntoAlbum(album: Album) { + album.applySongs(songs.filter { it.albumId == album.id }) + } + + private fun createArtistsFromAlbums(albums: List) { + val groupedAlbums = albums.groupBy { it.artistName } + + groupedAlbums.forEach { + logD(it.key) + artists.add( + Artist(id = artists.size.toLong(), name = it.key, albums = it.value) ) - - for (song in unknownSongs) { - song.album = unknownAlbum - unknownAlbum.songs.add(song) - } - - albums.add(unknownAlbum) - - logD("${unknownSongs.size} songs were placed into an unknown album.") } } - - private fun sortAlbumsIntoArtists() { - logD("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.artistId == artist.id } - - // Then add them to the artist, along with refreshing the amount of albums - for (album in artistAlbums) { - album.artist = artist - artist.albums.add(album) - } - - // Then group the artist's genres and sort them by "Prominence" - // A.K.A Who has the most bugged duplicate genres - val groupedGenres = artist.genres.groupBy { it.name } - artist.genres.clear() - - groupedGenres.keys.sortedByDescending { key -> - groupedGenres[key]?.size - }.forEach { key -> - groupedGenres[key]?.get(0)?.let { - artist.genres.add(it) - } - } - - 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 = Artist( - name = artistPlaceholder - ) - - for (album in unknownAlbums) { - album.artist = unknownArtist - unknownArtist.albums.add(album) - } - - artists.add(unknownArtist) - - logD("${unknownAlbums.size} albums were placed into an unknown artist.") - } - } - - private fun sortArtistsIntoGenres() { - logD("Sorting artists into genres...") - - val unknownArtists = artists.toMutableList() - - for (genre in genres) { - // Find all artists that match the current genre - val genreArtists = artists.filter { artist -> - artist.genres.any { - it.name == genre.name - } - } - - // Then add them to the genre, along with refreshing the amount of artists - genre.artists.addAll(genreArtists) - - unknownArtists.removeAll(genreArtists) - } - - if (unknownArtists.size > 0) { - // Reuse an existing unknown genre if one is found - val unknownGenre = genres.find { it.name == genrePlaceholder } ?: Genre( - name = genrePlaceholder - ) - - for (artist in unknownArtists) { - artist.genres.add(unknownGenre) - unknownGenre.artists.add(artist) - } - genres.add(unknownGenre) - - logD("${unknownArtists.size} artists were placed into an unknown genre.") - } - } - - // Band-aid any buggy genres created by the broken Music Loading system. - private fun fixBuggyGenres() { - // Remove genre duplicates at the end, as duplicate genres can be added during - // the sorting process as well. - genres = genres.distinctBy { - it.name - }.toMutableList() - - // Also eliminate any genres that don't have artists, which also happens sometimes. - genres.removeAll { it.artists.isEmpty() } - } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt index 799e991a3..de7111806 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt @@ -15,6 +15,7 @@ import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Header import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.recycler.SortMode import org.oxycblt.auxio.settings.SettingsManager import kotlin.random.Random @@ -143,14 +144,6 @@ class PlaybackStateManager private constructor() { * @param mode The [PlaybackMode] to construct the queue off of. */ fun playSong(song: Song, mode: PlaybackMode) { - // Auxio doesn't support playing songs while swapping the mode to GENRE, as its impossible - // to determine what genre a song has. - if (mode == PlaybackMode.IN_GENRE) { - logE("Auxio cant play songs with the mode of IN_GENRE.") - - return - } - logD("Updating song to ${song.name} and mode to $mode") val musicStore = MusicStore.getInstance() @@ -161,17 +154,19 @@ class PlaybackStateManager private constructor() { mQueue = musicStore.songs.toMutableList() } + PlaybackMode.IN_GENRE -> { + mParent = song.genre + mQueue = orderSongsInGenre(song.genre!!) + } + PlaybackMode.IN_ARTIST -> { mParent = song.album.artist - mQueue = song.album.artist.songs.toMutableList() + mQueue = orderSongsInArtist(song.album.artist) } PlaybackMode.IN_ALBUM -> { mParent = song.album - mQueue = song.album.songs - } - - else -> { + mQueue = orderSongsInAlbum(song.album) } } @@ -780,54 +775,12 @@ class PlaybackStateManager private constructor() { mParent = when (mMode) { PlaybackMode.IN_ALBUM -> mQueue.firstOrNull()?.album PlaybackMode.IN_ARTIST -> mQueue.firstOrNull()?.album?.artist - PlaybackMode.IN_GENRE -> getCommonGenre() + PlaybackMode.IN_GENRE -> mQueue.firstOrNull()?.genre PlaybackMode.ALL_SONGS -> null } } } - /** - * Search for the common genre out of a queue of songs that **should have a common genre**. - * @return The **single** common genre, null if there isn't any or if there's multiple. - */ - private fun getCommonGenre(): Genre? { - // Pool of "Possible" genres, these get narrowed down until the list is only - // the actual genre(s) that all songs in the queue have in common. - var genres = mutableListOf() - var otherGenres: MutableList - - for (queueSong in mQueue) { - // If there's still songs to check despite the pool of genres being empty, re-add them. - if (genres.size == 0) { - genres.addAll(queueSong.album.artist.genres) - continue - } - - otherGenres = genres.toMutableList() - - // Iterate through the current genres and remove the ones that don't exist in this song, - // narrowing down the pool of possible genres. - for (genre in genres) { - if (queueSong.album.artist.genres.find { it.id == genre.id } == null) { - otherGenres.remove(genre) - } - } - - genres = otherGenres.toMutableList() - } - - logD("Found genre $genres") - - // There should not be more than one common genre, so return null if that's the case - if (genres.size > 1) { - return null - } - - // Sometimes the narrowing process will lead to a zero-size list, so return null if that - // is the case. - return genres.firstOrNull() - } - // --- ORDERING FUNCTIONS --- /** @@ -866,17 +819,7 @@ class PlaybackStateManager private constructor() { * Create an ordered queue based on a [Genre]. */ private fun orderSongsInGenre(genre: Genre): MutableList { - val final = mutableListOf() - - genre.artists.sortedWith( - compareBy(String.CASE_INSENSITIVE_ORDER) { it.name } - ).forEach { artist -> - artist.albums.sortedByDescending { it.year }.forEach { album -> - final.addAll(album.songs.sortedBy { it.track }) - } - } - - return final + return SortMode.ALPHA_DOWN.getSortedSongList(genre.songs).toMutableList() } /** diff --git a/app/src/main/res/layout-land/fragment_album_detail.xml b/app/src/main/res/layout-land/fragment_album_detail.xml index b1eadc698..2be17bc5b 100644 --- a/app/src/main/res/layout-land/fragment_album_detail.xml +++ b/app/src/main/res/layout-land/fragment_album_detail.xml @@ -54,7 +54,7 @@ app:layout_constraintTop_toTopOf="parent" tools:src="@drawable/ic_album" /> - - - @@ -108,13 +108,13 @@ style="@style/HeaderAction" android:contentDescription="@string/description_sort_button" android:onClick="@{() -> detailModel.incrementGenreSortMode()}" - app:layout_constraintBottom_toBottomOf="@+id/genre_artist_header" + app:layout_constraintBottom_toBottomOf="@+id/genre_song_header" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="@+id/genre_artist_header" + app:layout_constraintTop_toTopOf="@+id/genre_song_header" tools:src="@drawable/ic_sort_alpha_down" /> + tools:listitem="@layout/item_song" /> diff --git a/app/src/main/res/layout/fragment_album_detail.xml b/app/src/main/res/layout/fragment_album_detail.xml index 5350decad..1f6594617 100644 --- a/app/src/main/res/layout/fragment_album_detail.xml +++ b/app/src/main/res/layout/fragment_album_detail.xml @@ -54,7 +54,7 @@ app:layout_constraintTop_toTopOf="parent" tools:src="@drawable/ic_album" /> - - + @@ -106,13 +107,13 @@ style="@style/HeaderAction" android:contentDescription="@string/description_sort_button" android:onClick="@{() -> detailModel.incrementGenreSortMode()}" - app:layout_constraintBottom_toBottomOf="@+id/genre_artist_header" + app:layout_constraintBottom_toBottomOf="@+id/genre_song_header" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="@+id/genre_artist_header" + app:layout_constraintTop_toTopOf="@+id/genre_song_header" tools:src="@drawable/ic_sort_alpha_down" /> + tools:listitem="@layout/item_song" /> diff --git a/app/src/main/res/layout/item_genre.xml b/app/src/main/res/layout/item_genre.xml index 1b431bc51..62495cce1 100644 --- a/app/src/main/res/layout/item_genre.xml +++ b/app/src/main/res/layout/item_genre.xml @@ -38,12 +38,12 @@ + tools:text="4 Albums, 40 Songs" /> \ No newline at end of file