music: refactor music loading
Refactor music loading to be based off of songs entirely. This reduces efficency but enables some nices fixes, notably: 1. Album artists now have basic support [You won't be able to see specific artists, but they won't be fragmented anymore] 2. Samsung devices probably shouldn't get confused about artist names anymore, like in #40 This should hopefully be the last time I need to refactor this horrible system. Thank god.
This commit is contained in:
parent
276f991b2b
commit
3ab425839c
10 changed files with 202 additions and 202 deletions
|
@ -30,6 +30,7 @@ import coil.size.Size
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.source
|
import okio.source
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
|
import org.oxycblt.auxio.music.toAlbumArtURI
|
||||||
import org.oxycblt.auxio.music.toURI
|
import org.oxycblt.auxio.music.toURI
|
||||||
import org.oxycblt.auxio.settings.SettingsManager
|
import org.oxycblt.auxio.settings.SettingsManager
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
|
@ -56,14 +57,15 @@ class AlbumArtFetcher(private val context: Context) : Fetcher<Album> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadMediaStoreCovers(data: Album): SourceResult {
|
private fun loadMediaStoreCovers(data: Album): SourceResult {
|
||||||
val stream = context.contentResolver.openInputStream(data.coverUri)
|
val uri = data.id.toAlbumArtURI()
|
||||||
|
val stream = context.contentResolver.openInputStream(uri)
|
||||||
|
|
||||||
if (stream != null) {
|
if (stream != null) {
|
||||||
// Don't close the stream here as it will cause an error later from an attempted read.
|
// Don't close the stream here as it will cause an error later from an attempted read.
|
||||||
// This stream still seems to close itself at some point, so its fine.
|
// This stream still seems to close itself at some point, so its fine.
|
||||||
return SourceResult(
|
return SourceResult(
|
||||||
source = stream.source().buffer(),
|
source = stream.source().buffer(),
|
||||||
mimeType = context.contentResolver.getType(data.coverUri),
|
mimeType = context.contentResolver.getType(uri),
|
||||||
dataSource = DataSource.DISK
|
dataSource = DataSource.DISK
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -97,5 +99,5 @@ class AlbumArtFetcher(private val context: Context) : Fetcher<Album> {
|
||||||
return loadMediaStoreCovers(data)
|
return loadMediaStoreCovers(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun key(data: Album) = data.coverUri.toString()
|
override fun key(data: Album) = data.id.toString()
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,7 @@ import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.Parent
|
import org.oxycblt.auxio.music.Parent
|
||||||
|
import org.oxycblt.auxio.music.toAlbumArtURI
|
||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
|
@ -57,11 +58,11 @@ class MosaicFetcher(private val context: Context) : Fetcher<Parent> {
|
||||||
|
|
||||||
when (data) {
|
when (data) {
|
||||||
is Artist -> data.albums.forEachIndexed { index, album ->
|
is Artist -> data.albums.forEachIndexed { index, album ->
|
||||||
if (index < 4) { uris.add(album.coverUri) }
|
if (index < 4) { uris.add(album.id.toAlbumArtURI()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
is Genre -> data.songs.groupBy { it.album.coverUri }.keys.forEachIndexed { index, uri ->
|
is Genre -> data.songs.groupBy { it.album.id }.keys.forEachIndexed { index, id ->
|
||||||
if (index < 4) { uris.add(uri) }
|
if (index < 4) { uris.add(id.toAlbumArtURI()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {}
|
else -> {}
|
||||||
|
|
|
@ -163,13 +163,13 @@ class HomeFragment : Fragment() {
|
||||||
homeModel.curTab.observe(viewLifecycleOwner) { tab ->
|
homeModel.curTab.observe(viewLifecycleOwner) { tab ->
|
||||||
binding.homeAppbar.liftOnScrollTargetViewId = when (requireNotNull(tab)) {
|
binding.homeAppbar.liftOnScrollTargetViewId = when (requireNotNull(tab)) {
|
||||||
DisplayMode.SHOW_SONGS -> {
|
DisplayMode.SHOW_SONGS -> {
|
||||||
updateSortMenu(sortItem, homeModel.songSortMode)
|
updateSortMenu(sortItem, tab)
|
||||||
|
|
||||||
R.id.home_song_list
|
R.id.home_song_list
|
||||||
}
|
}
|
||||||
|
|
||||||
DisplayMode.SHOW_ALBUMS -> {
|
DisplayMode.SHOW_ALBUMS -> {
|
||||||
updateSortMenu(sortItem, homeModel.albumSortMode) { id ->
|
updateSortMenu(sortItem, tab) { id ->
|
||||||
id != R.id.option_sort_album
|
id != R.id.option_sort_album
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,7 +177,7 @@ class HomeFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
DisplayMode.SHOW_ARTISTS -> {
|
DisplayMode.SHOW_ARTISTS -> {
|
||||||
updateSortMenu(sortItem, homeModel.artistSortMode) { id ->
|
updateSortMenu(sortItem, tab) { id ->
|
||||||
id == R.id.option_sort_asc || id == R.id.option_sort_dsc
|
id == R.id.option_sort_asc || id == R.id.option_sort_dsc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,7 +185,7 @@ class HomeFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
DisplayMode.SHOW_GENRES -> {
|
DisplayMode.SHOW_GENRES -> {
|
||||||
updateSortMenu(sortItem, homeModel.genreSortMode) { id ->
|
updateSortMenu(sortItem, tab) { id ->
|
||||||
id == R.id.option_sort_asc || id == R.id.option_sort_dsc
|
id == R.id.option_sort_asc || id == R.id.option_sort_dsc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -228,9 +228,11 @@ class HomeFragment : Fragment() {
|
||||||
|
|
||||||
private fun updateSortMenu(
|
private fun updateSortMenu(
|
||||||
item: MenuItem,
|
item: MenuItem,
|
||||||
toHighlight: SortMode,
|
displayMode: DisplayMode,
|
||||||
isVisible: (Int) -> Boolean = { true }
|
isVisible: (Int) -> Boolean = { true }
|
||||||
) {
|
) {
|
||||||
|
val toHighlight = homeModel.getSortForDisplay(displayMode)
|
||||||
|
|
||||||
for (option in item.subMenu) {
|
for (option in item.subMenu) {
|
||||||
if (option.itemId == toHighlight.itemId) {
|
if (option.itemId == toHighlight.itemId) {
|
||||||
option.isChecked = true
|
option.isChecked = true
|
||||||
|
|
|
@ -26,6 +26,7 @@ import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
import org.oxycblt.auxio.music.MusicStore
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
import org.oxycblt.auxio.settings.SettingsManager
|
||||||
import org.oxycblt.auxio.ui.DisplayMode
|
import org.oxycblt.auxio.ui.DisplayMode
|
||||||
import org.oxycblt.auxio.ui.SortMode
|
import org.oxycblt.auxio.ui.SortMode
|
||||||
|
|
||||||
|
@ -56,19 +57,13 @@ class HomeViewModel : ViewModel() {
|
||||||
private val mCurTab = MutableLiveData(mTabs.value!![0])
|
private val mCurTab = MutableLiveData(mTabs.value!![0])
|
||||||
val curTab: LiveData<DisplayMode> = mCurTab
|
val curTab: LiveData<DisplayMode> = mCurTab
|
||||||
|
|
||||||
var genreSortMode = SortMode.ASCENDING
|
private var genreSortMode = SortMode.ASCENDING
|
||||||
private set
|
private var artistSortMode = SortMode.ASCENDING
|
||||||
|
private var albumSortMode = SortMode.ASCENDING
|
||||||
var artistSortMode = SortMode.ASCENDING
|
private var songSortMode = SortMode.ASCENDING
|
||||||
private set
|
|
||||||
|
|
||||||
var albumSortMode = SortMode.ASCENDING
|
|
||||||
private set
|
|
||||||
|
|
||||||
var songSortMode = SortMode.ASCENDING
|
|
||||||
private set
|
|
||||||
|
|
||||||
private val musicStore = MusicStore.getInstance()
|
private val musicStore = MusicStore.getInstance()
|
||||||
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
mSongs.value = songSortMode.sortSongs(musicStore.songs)
|
mSongs.value = songSortMode.sortSongs(musicStore.songs)
|
||||||
|
@ -86,6 +81,15 @@ class HomeViewModel : ViewModel() {
|
||||||
mCurTab.value = mode
|
mCurTab.value = mode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getSortForDisplay(displayMode: DisplayMode): SortMode {
|
||||||
|
return when (displayMode) {
|
||||||
|
DisplayMode.SHOW_GENRES -> genreSortMode
|
||||||
|
DisplayMode.SHOW_ARTISTS -> artistSortMode
|
||||||
|
DisplayMode.SHOW_ALBUMS -> albumSortMode
|
||||||
|
DisplayMode.SHOW_SONGS -> songSortMode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the currently displayed item's [SortMode].
|
* Update the currently displayed item's [SortMode].
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.music
|
package org.oxycblt.auxio.music
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
@ -87,7 +86,10 @@ data class Song(
|
||||||
override val id: Long,
|
override val id: Long,
|
||||||
override val name: String,
|
override val name: String,
|
||||||
val fileName: String,
|
val fileName: String,
|
||||||
|
val albumName: String,
|
||||||
val albumId: Long,
|
val albumId: Long,
|
||||||
|
val artistName: String,
|
||||||
|
val year: Int,
|
||||||
val track: Int,
|
val track: Int,
|
||||||
val duration: Long
|
val duration: Long
|
||||||
) : BaseModel(), Hashable {
|
) : BaseModel(), Hashable {
|
||||||
|
@ -97,8 +99,8 @@ data class Song(
|
||||||
val genre: Genre? get() = mGenre
|
val genre: Genre? get() = mGenre
|
||||||
val album: Album get() = requireNotNull(mAlbum)
|
val album: Album get() = requireNotNull(mAlbum)
|
||||||
|
|
||||||
val seconds = duration / 1000
|
val seconds: Long get() = duration / 1000
|
||||||
val formattedDuration = seconds.toDuration()
|
val formattedDuration: String get() = (duration / 1000).toDuration()
|
||||||
|
|
||||||
override val hash: Int get() {
|
override val hash: Int get() {
|
||||||
var result = name.hashCode()
|
var result = name.hashCode()
|
||||||
|
@ -108,22 +110,17 @@ data class Song(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun linkAlbum(album: Album) {
|
fun linkAlbum(album: Album) {
|
||||||
if (mAlbum == null) {
|
mAlbum = album
|
||||||
mAlbum = album
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun linkGenre(genre: Genre) {
|
fun linkGenre(genre: Genre) {
|
||||||
if (mGenre == null) {
|
mGenre = genre
|
||||||
mGenre = genre
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The data object for an album. Inherits [Parent].
|
* The data object for an album. Inherits [Parent].
|
||||||
* @property artistName The name of the parent artist. Do not use this outside of creating the artist from albums
|
* @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 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 artist The Album's parent [Artist]. use this instead of [artistName]
|
||||||
* @property songs The Album's child [Song]s.
|
* @property songs The Album's child [Song]s.
|
||||||
|
@ -133,15 +130,18 @@ data class Album(
|
||||||
override val id: Long,
|
override val id: Long,
|
||||||
override val name: String,
|
override val name: String,
|
||||||
val artistName: String,
|
val artistName: String,
|
||||||
val coverUri: Uri,
|
val year: Int,
|
||||||
val year: Int
|
val songs: List<Song>
|
||||||
) : Parent() {
|
) : Parent() {
|
||||||
|
init {
|
||||||
|
songs.forEach { song ->
|
||||||
|
song.linkAlbum(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var mArtist: Artist? = null
|
private var mArtist: Artist? = null
|
||||||
val artist: Artist get() = requireNotNull(mArtist)
|
val artist: Artist get() = requireNotNull(mArtist)
|
||||||
|
|
||||||
private val mSongs = mutableListOf<Song>()
|
|
||||||
val songs: List<Song> get() = mSongs
|
|
||||||
|
|
||||||
val totalDuration: String get() =
|
val totalDuration: String get() =
|
||||||
songs.sumOf { it.seconds }.toDuration()
|
songs.sumOf { it.seconds }.toDuration()
|
||||||
|
|
||||||
|
@ -155,13 +155,6 @@ data class Album(
|
||||||
fun linkArtist(artist: Artist) {
|
fun linkArtist(artist: Artist) {
|
||||||
mArtist = artist
|
mArtist = artist
|
||||||
}
|
}
|
||||||
|
|
||||||
fun linkSongs(songs: List<Song>) {
|
|
||||||
for (song in songs) {
|
|
||||||
song.linkAlbum(this)
|
|
||||||
mSongs.add(song)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -175,6 +168,12 @@ data class Artist(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
val albums: List<Album>
|
val albums: List<Album>
|
||||||
) : Parent() {
|
) : Parent() {
|
||||||
|
init {
|
||||||
|
albums.forEach { album ->
|
||||||
|
album.linkArtist(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val genre: Genre? by lazy {
|
val genre: Genre? by lazy {
|
||||||
// Get the genre that corresponds to the most songs in this artist, which would be
|
// Get the genre that corresponds to the most songs in this artist, which would be
|
||||||
// the most "Prominent" genre.
|
// the most "Prominent" genre.
|
||||||
|
@ -186,12 +185,6 @@ data class Artist(
|
||||||
}
|
}
|
||||||
|
|
||||||
override val hash = name.hashCode()
|
override val hash = name.hashCode()
|
||||||
|
|
||||||
init {
|
|
||||||
albums.forEach { album ->
|
|
||||||
album.linkArtist(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -20,18 +20,74 @@ package org.oxycblt.auxio.music
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
|
||||||
import android.provider.MediaStore
|
|
||||||
import android.provider.MediaStore.Audio.Albums
|
|
||||||
import android.provider.MediaStore.Audio.Genres
|
import android.provider.MediaStore.Audio.Genres
|
||||||
import android.provider.MediaStore.Audio.Media
|
import android.provider.MediaStore.Audio.Media
|
||||||
import androidx.core.database.getStringOrNull
|
import androidx.core.database.getStringOrNull
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.excluded.ExcludedDatabase
|
import org.oxycblt.auxio.excluded.ExcludedDatabase
|
||||||
|
import org.oxycblt.auxio.ui.SortMode
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class that loads/constructs [Genre]s, [Artist]s, [Album]s, and [Song] objects from the filesystem
|
* This class does pretty much all the black magic required to get a remotely sensible music
|
||||||
|
* indexing system while still optimizing for time. I would recommend you leave this file now
|
||||||
|
* before you lose your sanity trying to understand the hoops I had to jump through for this
|
||||||
|
* system, but if you really want to stay, here's a debrief on why this code is so awful.
|
||||||
|
*
|
||||||
|
* MediaStore is not a good API. It is not even a bad API. Calling it a bad API is an insult to
|
||||||
|
* other bad android APIs, like CoordinatorLayout or InputMethodManager. No. MediaStore is a
|
||||||
|
* crime against humanity and probably a way to summon Zalgo if you tried hard enough.
|
||||||
|
*
|
||||||
|
* You think that if you wanted to query a song's genre from a media database, you could just
|
||||||
|
* put "genre" in the query and it would return it, right? But not with MediaStore! No, that's
|
||||||
|
* to straightfoward for this platform. So instead, you have to query for each genre, query all
|
||||||
|
* the songs in each genre, and then iterate through those songs to link every song with their
|
||||||
|
* genre. This is not documented anywhere in MediaStore's documentation, and the O(mom im scared)
|
||||||
|
* algorithm you have to run to get it working single-handedly DOUBLES Auxio's loading times. At
|
||||||
|
* no point have the devs considered that this column is absolutely busted, and instead focused on
|
||||||
|
* adding infuriat- I mean nice proprietary extensions to MediaStore for their own Google Play Music,
|
||||||
|
* and we all know how great that worked out!
|
||||||
|
*
|
||||||
|
* It's not even ergonomics that makes this API bad. It's base implementation is completely borked
|
||||||
|
* as well. Did you know that MediaStore doesn't accept dates that aren't from ID3v2.3 MP3 files?
|
||||||
|
* I sure didn't, until I decided to upgrade my music collection to ID3v2.4 and Xiph only to see
|
||||||
|
* that their metadata parser has a brain aneurysm the moment it stumbles upon a dreaded TRDC or
|
||||||
|
* DATE tag. Once again, this is because internally android uses an ancient in-house metadata
|
||||||
|
* parser to get everything indexed, and so far they have not bothered to modernize this parser
|
||||||
|
* or even switch it to something more powerful like Taglib, not even in Android 12. ID3v2.4 is
|
||||||
|
* 21 years old. It can drink now. All my what.
|
||||||
|
*
|
||||||
|
* Not to mention all the other infuriating quirks. Album artists can't be accessed from the albums
|
||||||
|
* table, so we have to go for the less efficent "make a big query on all the songs lol" method
|
||||||
|
* so that songs don't end up fragmented across artists. Pretty much every OEM has added some
|
||||||
|
* extension or quirk to MediaStore that I cannot determine, with some OEMs (COUGHSAMSUNGCOUGH)
|
||||||
|
* crippling the normal tables so that you're railroaded into their ad-infested music app.
|
||||||
|
* The way I do blacklisting relies on a deprecated method, and the supposedly "modern" method
|
||||||
|
* is SLOWER and causes even more problems since I have to manage databases across version
|
||||||
|
* boundaries. Sometimes music will have a deformed clone that I can't filter out, sometimes
|
||||||
|
* Genre's will just break for no reason, sometimes this plate of spaghetti just completely breaks
|
||||||
|
* down and is unable to get any metadata. Everything is broken in it's own special unique way and
|
||||||
|
* I absolutely hate it.
|
||||||
|
*
|
||||||
|
* Is there anything we can do about it? No. Google has routinely shut down issues that beg google
|
||||||
|
* to fix glaring issues with MediaStore or to just take the API behind the woodshed and shoot it.
|
||||||
|
* Largely because they have zero incentive to improve it, especially for such obscure things
|
||||||
|
* as indexing music. As a result, some players like Vanilla and VLC just hack their own pidgin
|
||||||
|
* version of MediaStore from their own parsers, but this is both infeasible for Auxio due to how
|
||||||
|
* incredibly slow it is to get a file handle from the android sandbox AND how much harder it is
|
||||||
|
* to manage a database of your own media that mirrors the filesystem perfectly. And even if I set
|
||||||
|
* aside those crippling issues and changed me indexer to that, it would face the even larger
|
||||||
|
* problem of how google keeps trying to kill the filesystem and force you into their
|
||||||
|
* ContentResolver API. In the future MediaStore could be the only system we have, which is also the
|
||||||
|
* day that greenland melts and birthdays stop happening forever.
|
||||||
|
*
|
||||||
|
* I'm pretty sure nothing is going to happen and MediaStore will continue to be neglected and
|
||||||
|
* probably deprecated eventually for a "new" API that just coincidentally excludes music indexing.
|
||||||
|
* Because go **** yourself for wanting to listen to music you own. Be a good consoomer and listen
|
||||||
|
* to your AlgoMix MusikStream™.
|
||||||
|
*
|
||||||
|
* I hate this platform so much.
|
||||||
|
*
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class MusicLoader(private val context: Context) {
|
class MusicLoader(private val context: Context) {
|
||||||
|
@ -53,12 +109,11 @@ class MusicLoader(private val context: Context) {
|
||||||
buildSelector()
|
buildSelector()
|
||||||
|
|
||||||
loadGenres()
|
loadGenres()
|
||||||
loadAlbums()
|
|
||||||
loadSongs()
|
loadSongs()
|
||||||
|
|
||||||
linkAlbums()
|
|
||||||
buildArtists()
|
|
||||||
linkGenres()
|
linkGenres()
|
||||||
|
|
||||||
|
buildAlbums()
|
||||||
|
buildArtists()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
|
@ -95,7 +150,8 @@ class MusicLoader(private val context: Context) {
|
||||||
|
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
val id = cursor.getLong(idIndex)
|
val id = cursor.getLong(idIndex)
|
||||||
val name = cursor.getStringOrNull(nameIndex) ?: continue // No non-broken genre would be missing a name
|
// No non-broken genre would be missing a name.
|
||||||
|
val name = cursor.getStringOrNull(nameIndex) ?: continue
|
||||||
|
|
||||||
genres.add(Genre(id, name))
|
genres.add(Genre(id, name))
|
||||||
}
|
}
|
||||||
|
@ -104,53 +160,6 @@ class MusicLoader(private val context: Context) {
|
||||||
logD("Genre search finished with ${genres.size} genres found.")
|
logD("Genre search finished with ${genres.size} genres found.")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadAlbums() {
|
|
||||||
logD("Starting album search...")
|
|
||||||
|
|
||||||
val albumCursor = resolver.query(
|
|
||||||
Albums.EXTERNAL_CONTENT_URI,
|
|
||||||
arrayOf(
|
|
||||||
Albums._ID, // 0
|
|
||||||
Albums.ALBUM, // 1
|
|
||||||
Albums.ARTIST, // 2
|
|
||||||
Albums.LAST_YEAR, // 4
|
|
||||||
),
|
|
||||||
null, null,
|
|
||||||
Albums.DEFAULT_SORT_ORDER
|
|
||||||
)
|
|
||||||
|
|
||||||
val albumPlaceholder = context.getString(R.string.def_album)
|
|
||||||
val artistPlaceholder = context.getString(R.string.def_artist)
|
|
||||||
|
|
||||||
albumCursor?.use { cursor ->
|
|
||||||
val idIndex = cursor.getColumnIndexOrThrow(Albums._ID)
|
|
||||||
val nameIndex = cursor.getColumnIndexOrThrow(Albums.ALBUM)
|
|
||||||
val artistNameIndex = cursor.getColumnIndexOrThrow(Albums.ARTIST)
|
|
||||||
val yearIndex = cursor.getColumnIndexOrThrow(Albums.LAST_YEAR)
|
|
||||||
|
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
val id = cursor.getLong(idIndex)
|
|
||||||
val name = cursor.getString(nameIndex) ?: albumPlaceholder
|
|
||||||
var artistName = cursor.getString(artistNameIndex) ?: artistPlaceholder
|
|
||||||
val year = cursor.getInt(yearIndex)
|
|
||||||
val coverUri = id.toAlbumArtURI()
|
|
||||||
|
|
||||||
// Correct any artist names to a nicer "Unknown Artist" label
|
|
||||||
if (artistName == MediaStore.UNKNOWN_STRING) {
|
|
||||||
artistName = artistPlaceholder
|
|
||||||
}
|
|
||||||
|
|
||||||
albums.add(Album(id, name, artistName, coverUri, year))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
albums = albums.distinctBy {
|
|
||||||
it.name to it.artistName to it.year
|
|
||||||
}.toMutableList()
|
|
||||||
|
|
||||||
logD("Album search finished with ${albums.size} albums found")
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
private fun loadSongs() {
|
private fun loadSongs() {
|
||||||
logD("Starting song search...")
|
logD("Starting song search...")
|
||||||
|
@ -159,11 +168,15 @@ class MusicLoader(private val context: Context) {
|
||||||
Media.EXTERNAL_CONTENT_URI,
|
Media.EXTERNAL_CONTENT_URI,
|
||||||
arrayOf(
|
arrayOf(
|
||||||
Media._ID, // 0
|
Media._ID, // 0
|
||||||
Media.DISPLAY_NAME, // 1
|
Media.TITLE, // 1
|
||||||
Media.TITLE, // 2
|
Media.DISPLAY_NAME, // 2
|
||||||
Media.ALBUM_ID, // 3
|
Media.ALBUM, // 3
|
||||||
Media.TRACK, // 4
|
Media.ALBUM_ID, // 4
|
||||||
Media.DURATION, // 5
|
Media.ARTIST, // 5
|
||||||
|
Media.ALBUM_ARTIST, // 6
|
||||||
|
Media.YEAR, // 7
|
||||||
|
Media.TRACK, // 8
|
||||||
|
Media.DURATION, // 9
|
||||||
),
|
),
|
||||||
selector, args,
|
selector, args,
|
||||||
Media.DEFAULT_SORT_ORDER
|
Media.DEFAULT_SORT_ORDER
|
||||||
|
@ -173,7 +186,11 @@ class MusicLoader(private val context: Context) {
|
||||||
val idIndex = cursor.getColumnIndexOrThrow(Media._ID)
|
val idIndex = cursor.getColumnIndexOrThrow(Media._ID)
|
||||||
val titleIndex = cursor.getColumnIndexOrThrow(Media.TITLE)
|
val titleIndex = cursor.getColumnIndexOrThrow(Media.TITLE)
|
||||||
val fileIndex = cursor.getColumnIndexOrThrow(Media.DISPLAY_NAME)
|
val fileIndex = cursor.getColumnIndexOrThrow(Media.DISPLAY_NAME)
|
||||||
val albumIndex = cursor.getColumnIndexOrThrow(Media.ALBUM_ID)
|
val albumIndex = cursor.getColumnIndexOrThrow(Media.ALBUM)
|
||||||
|
val albumIdIndex = cursor.getColumnIndexOrThrow(Media.ALBUM_ID)
|
||||||
|
val artistIndex = cursor.getColumnIndexOrThrow(Media.ARTIST)
|
||||||
|
val albumArtistIndex = cursor.getColumnIndexOrThrow(Media.ALBUM_ARTIST)
|
||||||
|
val yearIndex = cursor.getColumnIndexOrThrow(Media.YEAR)
|
||||||
val trackIndex = cursor.getColumnIndexOrThrow(Media.TRACK)
|
val trackIndex = cursor.getColumnIndexOrThrow(Media.TRACK)
|
||||||
val durationIndex = cursor.getColumnIndexOrThrow(Media.DURATION)
|
val durationIndex = cursor.getColumnIndexOrThrow(Media.DURATION)
|
||||||
|
|
||||||
|
@ -181,111 +198,89 @@ class MusicLoader(private val context: Context) {
|
||||||
val id = cursor.getLong(idIndex)
|
val id = cursor.getLong(idIndex)
|
||||||
val fileName = cursor.getString(fileIndex)
|
val fileName = cursor.getString(fileIndex)
|
||||||
val title = cursor.getString(titleIndex) ?: fileName
|
val title = cursor.getString(titleIndex) ?: fileName
|
||||||
val albumId = cursor.getLong(albumIndex)
|
val album = cursor.getString(albumIndex)
|
||||||
|
val albumId = cursor.getLong(albumIdIndex)
|
||||||
|
|
||||||
|
// MediaStore does not have support for artists in the album field, so we have to
|
||||||
|
// detect it on a song-by-song basis. This is another massive bottleneck in the music
|
||||||
|
// loader since we have to do a massive query to get what we want, but theres not
|
||||||
|
// a lot I can do that doesn't degrade UX.
|
||||||
|
val artist = cursor.getStringOrNull(albumArtistIndex)
|
||||||
|
?: cursor.getString(artistIndex)
|
||||||
|
|
||||||
|
val year = cursor.getInt(yearIndex)
|
||||||
val track = cursor.getInt(trackIndex)
|
val track = cursor.getInt(trackIndex)
|
||||||
val duration = cursor.getLong(durationIndex)
|
val duration = cursor.getLong(durationIndex)
|
||||||
|
|
||||||
songs.add(Song(id, title, fileName, albumId, track, duration))
|
songs.add(
|
||||||
|
Song(
|
||||||
|
id, title, fileName, album, albumId, artist, year, track, duration
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
songs = songs.distinctBy {
|
songs = songs.distinctBy {
|
||||||
it.name to it.albumId to it.track to it.duration
|
it.name to it.albumId to it.artistName to it.track to it.duration
|
||||||
}.toMutableList()
|
}.toMutableList()
|
||||||
|
|
||||||
logD("Song search finished with ${songs.size} found")
|
logD("Song search finished with ${songs.size} found")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun linkAlbums() {
|
private fun buildAlbums() {
|
||||||
logD("Linking albums")
|
logD("Linking albums")
|
||||||
|
|
||||||
// Group up songs by their album ids and then link them with their albums
|
// Group up songs by their album ids and then link them with their albums
|
||||||
val songsByAlbum = songs.groupBy { it.albumId }
|
val songsByAlbum = songs.groupBy { it.albumId }
|
||||||
val unknownAlbum = Album(
|
|
||||||
id = -1,
|
|
||||||
name = context.getString(R.string.def_album),
|
|
||||||
artistName = context.getString(R.string.def_artist),
|
|
||||||
coverUri = Uri.EMPTY,
|
|
||||||
year = 0
|
|
||||||
)
|
|
||||||
|
|
||||||
songsByAlbum.forEach { entry ->
|
songsByAlbum.forEach { entry ->
|
||||||
(albums.find { it.id == entry.key } ?: unknownAlbum).linkSongs(entry.value)
|
// Rely on the first song in this list for album information.
|
||||||
|
// Note: This might result in a bad year being used for an album if an album's songs
|
||||||
|
// have multiple years. This is fixable but is currently omitted for speed.
|
||||||
|
val song = entry.value[0]
|
||||||
|
|
||||||
|
albums.add(
|
||||||
|
Album(
|
||||||
|
id = entry.key,
|
||||||
|
name = song.albumName,
|
||||||
|
artistName = song.artistName,
|
||||||
|
songs = entry.value,
|
||||||
|
year = song.year
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
albums.removeAll { it.songs.isEmpty() }
|
albums.removeAll { it.songs.isEmpty() }
|
||||||
|
albums = SortMode.ASCENDING.sortAlbums(albums).toMutableList()
|
||||||
|
|
||||||
// If something goes horribly wrong and somehow songs are still not linked up by the
|
logD("Songs successfully linked into ${albums.size} albums")
|
||||||
// album id, just throw them into an unknown album.
|
|
||||||
if (unknownAlbum.songs.isNotEmpty()) {
|
|
||||||
albums.add(unknownAlbum)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildArtists() {
|
private fun buildArtists() {
|
||||||
logD("Linking artists")
|
logD("Linking artists")
|
||||||
|
|
||||||
// Group albums up by their artist name, should not result in any null-artist issues
|
|
||||||
val albumsByArtist = albums.groupBy { it.artistName }
|
val albumsByArtist = albums.groupBy { it.artistName }
|
||||||
|
|
||||||
albumsByArtist.forEach { entry ->
|
albumsByArtist.forEach { entry ->
|
||||||
|
// Because of our hacky album artist system, MediaStore artist IDs are unreliable.
|
||||||
|
// Therefore we just use the hashCode of the artist name as our ID and move on.
|
||||||
artists.add(
|
artists.add(
|
||||||
// IDs are incremented from the minimum int value so that they remain unique.
|
|
||||||
Artist(
|
Artist(
|
||||||
id = (Int.MIN_VALUE + artists.size).toLong(),
|
id = entry.key.hashCode().toLong(),
|
||||||
name = entry.key,
|
name = entry.key,
|
||||||
albums = entry.value
|
albums = entry.value
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
artists = SortMode.ASCENDING.sortModels(artists).toMutableList()
|
||||||
|
|
||||||
logD("Albums successfully linked into ${artists.size} artists")
|
logD("Albums successfully linked into ${artists.size} artists")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun linkGenres() {
|
private fun linkGenres() {
|
||||||
logD("Linking genres")
|
logD("Linking genres")
|
||||||
|
|
||||||
/*
|
|
||||||
* Okay, I'm going to go on a bit of a tangent here because this bit of code infuriates me.
|
|
||||||
*
|
|
||||||
* In any reasonable platform, you think that you could just query a media database for the
|
|
||||||
* "genre" field and get the genre for a song, right? But not with android! No. Thats too
|
|
||||||
* normal for this dysfunctional SDK that was dropped on it's head as a baby. Instead, we
|
|
||||||
* have to iterate through EACH GENRE, QUERY THE MEDIA DATABASE FOR THE SONGS OF THAT GENRE,
|
|
||||||
* AND THEN ITERATE THROUGH THAT LIST TO LINK EVERY SONG WITH THE GENRE. This O(mom im scared)
|
|
||||||
* algorithm single-handedly DOUBLES the amount of time it takes Auxio to load music, but
|
|
||||||
* apparently this is the only way you can have a remotely sensible genre system on this
|
|
||||||
* busted OS. Why is it this way? Nobody knows! Now this quirk is immortalized and has to
|
|
||||||
* be replicated in all future versions of this API, because god forbid you break some
|
|
||||||
* app that's probably older than some of the people reading this by now.
|
|
||||||
*
|
|
||||||
* Another fun fact, did you know that you can only get the date from ID3v2.3 MPEG files?
|
|
||||||
* I sure didn't, until I decided to update my music collection to ID3v2.4 and Xiph only
|
|
||||||
* to see that android apparently has a brain aneurysm the moment it sees a dreaded TDRC
|
|
||||||
* or DATE tag. This bug is similarly baked into the platform and hasnt been fixed, even
|
|
||||||
* in android TWELVE. ID3v2.4 has existed for TWENTY-ONE YEARS. IT CAN DRINK NOW. At least
|
|
||||||
* you could replace your ancient dinosaur parser with Taglib or something, but again,
|
|
||||||
* google cant bear even slighting the gods of backwards compat.
|
|
||||||
*
|
|
||||||
* Is there anything we can do about this system? No. Google's issue tracker, in classic
|
|
||||||
* google fashion, requires a google account to even view. Even if I were to set aside my
|
|
||||||
* Torvalds-esqe convictions and make an account, MediaStore is such an obscure part of the
|
|
||||||
* platform that it basically receives no attention compared to the advertising APIs or the
|
|
||||||
* nineteenth UI rework, and its not like the many music player developers are going to band
|
|
||||||
* together to beg google to take this API behind the woodshed and shoot it. So instead,
|
|
||||||
* players like VLC and Vanilla just hack their own pidgin version of MediaStore off of
|
|
||||||
* their own media parsers, but even this becomes increasingly impossible as google
|
|
||||||
* continues to kill the filesystem ala iOS. In the future MediaStore could be the only
|
|
||||||
* system we have, which is also the day greenland melts and birthdays stop happening
|
|
||||||
* forever. I'm pretty sure that at this point nothing is going to happen, google will
|
|
||||||
* continue to neglect MediaStore, and all the people who just want to listen to their music
|
|
||||||
* collections on their phone will continue to get screwed. But hey, at least some dev at
|
|
||||||
* google got a cushy managerial position where they can tweet about politics all day for
|
|
||||||
* shipping the brand new androidx.FooBarBlasterView, yay!
|
|
||||||
*
|
|
||||||
* I hate this platform so much.
|
|
||||||
*/
|
|
||||||
|
|
||||||
genres.forEach { genre ->
|
genres.forEach { genre ->
|
||||||
val songCursor = resolver.query(
|
val songCursor = resolver.query(
|
||||||
Genres.Members.getContentUri("external", genre.id),
|
Genres.Members.getContentUri("external", genre.id),
|
||||||
|
@ -306,22 +301,21 @@ class MusicLoader(private val context: Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Any songs without genres will be thrown into an unknown genre
|
// Songs that don't have a genre will be thrown into an unknown genre.
|
||||||
val songsWithoutGenres = songs.filter { it.genre == null }
|
|
||||||
|
|
||||||
if (songsWithoutGenres.isNotEmpty()) {
|
val unknownGenre = Genre(
|
||||||
val unknownGenre = Genre(
|
id = -2,
|
||||||
id = -2,
|
name = context.getString(R.string.def_genre)
|
||||||
name = context.getString(R.string.def_genre)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
songsWithoutGenres.forEach { song ->
|
songs.forEach { song ->
|
||||||
|
if (song.genre == null) {
|
||||||
unknownGenre.linkSong(song)
|
unknownGenre.linkSong(song)
|
||||||
}
|
}
|
||||||
|
|
||||||
genres.add(unknownGenre)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
genres.removeAll { it.songs.isEmpty() }
|
if (unknownGenre.songs.isEmpty()) {
|
||||||
|
genres.add(unknownGenre)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,17 +41,13 @@ enum class LoopMode {
|
||||||
* @return The int constant for this mode
|
* @return The int constant for this mode
|
||||||
*/
|
*/
|
||||||
fun toInt(): Int {
|
fun toInt(): Int {
|
||||||
return when (this) {
|
return CONST_NONE + ordinal
|
||||||
NONE -> CONST_NONE
|
|
||||||
ALL -> CONST_ALL
|
|
||||||
TRACK -> CONST_TRACK
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val CONST_NONE = 0xA100
|
private const val CONST_NONE = 0xA100
|
||||||
const val CONST_ALL = 0xA101
|
private const val CONST_ALL = 0xA101
|
||||||
const val CONST_TRACK = 0xA102
|
private const val CONST_TRACK = 0xA102
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert an int [constant] into a LoopMode, or null if it isnt valid.
|
* Convert an int [constant] into a LoopMode, or null if it isnt valid.
|
||||||
|
|
|
@ -37,19 +37,14 @@ enum class PlaybackMode {
|
||||||
* @return The constant for this mode,
|
* @return The constant for this mode,
|
||||||
*/
|
*/
|
||||||
fun toInt(): Int {
|
fun toInt(): Int {
|
||||||
return when (this) {
|
return CONST_IN_ARTIST + ordinal
|
||||||
IN_ARTIST -> CONST_IN_ARTIST
|
|
||||||
IN_GENRE -> CONST_IN_GENRE
|
|
||||||
IN_ALBUM -> CONST_IN_ALBUM
|
|
||||||
ALL_SONGS -> CONST_ALL_SONGS
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val CONST_IN_GENRE = 0xA103
|
private const val CONST_IN_GENRE = 0xA103
|
||||||
const val CONST_IN_ARTIST = 0xA104
|
private const val CONST_IN_ARTIST = 0xA104
|
||||||
const val CONST_IN_ALBUM = 0xA105
|
private const val CONST_IN_ALBUM = 0xA105
|
||||||
const val CONST_ALL_SONGS = 0xA106
|
private const val CONST_ALL_SONGS = 0xA106
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a [PlaybackMode] for an int [constant]
|
* Get a [PlaybackMode] for an int [constant]
|
||||||
|
|
|
@ -32,6 +32,9 @@ class DiffCallback<T : BaseModel> : DiffUtil.ItemCallback<T>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun areItemsTheSame(oldItem: T, newItem: T): Boolean {
|
override fun areItemsTheSame(oldItem: T, newItem: T): Boolean {
|
||||||
|
// Prevent ID collisions from occurring between datatypes.
|
||||||
|
if (oldItem.javaClass != newItem.javaClass) return false
|
||||||
|
|
||||||
return oldItem.id == newItem.id
|
return oldItem.id == newItem.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,11 @@ enum class DisplayMode {
|
||||||
private const val CONST_SHOW_ALBUMS = 0xA10A
|
private const val CONST_SHOW_ALBUMS = 0xA10A
|
||||||
private const val CONST_SHOW_SONGS = 0xA10B
|
private const val CONST_SHOW_SONGS = 0xA10B
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert this enum into an integer for filtering.
|
||||||
|
* In this context, a null value means to filter nothing.
|
||||||
|
* @return An integer constant for that display mode, or a constant for a null [DisplayMode]
|
||||||
|
*/
|
||||||
fun toFilterInt(value: DisplayMode?): Int {
|
fun toFilterInt(value: DisplayMode?): Int {
|
||||||
return when (value) {
|
return when (value) {
|
||||||
SHOW_SONGS -> CONST_SHOW_SONGS
|
SHOW_SONGS -> CONST_SHOW_SONGS
|
||||||
|
@ -45,6 +50,11 @@ enum class DisplayMode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a filtering integer to a [DisplayMode].
|
||||||
|
* In this context, a null value means to filter nothing.
|
||||||
|
* @return A [DisplayMode] for this constant (including null)
|
||||||
|
*/
|
||||||
fun fromFilterInt(value: Int): DisplayMode? {
|
fun fromFilterInt(value: Int): DisplayMode? {
|
||||||
return when (value) {
|
return when (value) {
|
||||||
CONST_SHOW_SONGS -> SHOW_SONGS
|
CONST_SHOW_SONGS -> SHOW_SONGS
|
||||||
|
|
Loading…
Reference in a new issue