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.source
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.toAlbumArtURI
|
||||
import org.oxycblt.auxio.music.toURI
|
||||
import org.oxycblt.auxio.settings.SettingsManager
|
||||
import java.io.ByteArrayInputStream
|
||||
|
@ -56,14 +57,15 @@ class AlbumArtFetcher(private val context: Context) : Fetcher<Album> {
|
|||
}
|
||||
|
||||
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) {
|
||||
// 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.
|
||||
return SourceResult(
|
||||
source = stream.source().buffer(),
|
||||
mimeType = context.contentResolver.getType(data.coverUri),
|
||||
mimeType = context.contentResolver.getType(uri),
|
||||
dataSource = DataSource.DISK
|
||||
)
|
||||
}
|
||||
|
@ -97,5 +99,5 @@ class AlbumArtFetcher(private val context: Context) : Fetcher<Album> {
|
|||
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.Genre
|
||||
import org.oxycblt.auxio.music.Parent
|
||||
import org.oxycblt.auxio.music.toAlbumArtURI
|
||||
import java.io.Closeable
|
||||
import java.io.InputStream
|
||||
|
||||
|
@ -57,11 +58,11 @@ class MosaicFetcher(private val context: Context) : Fetcher<Parent> {
|
|||
|
||||
when (data) {
|
||||
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 ->
|
||||
if (index < 4) { uris.add(uri) }
|
||||
is Genre -> data.songs.groupBy { it.album.id }.keys.forEachIndexed { index, id ->
|
||||
if (index < 4) { uris.add(id.toAlbumArtURI()) }
|
||||
}
|
||||
|
||||
else -> {}
|
||||
|
|
|
@ -163,13 +163,13 @@ class HomeFragment : Fragment() {
|
|||
homeModel.curTab.observe(viewLifecycleOwner) { tab ->
|
||||
binding.homeAppbar.liftOnScrollTargetViewId = when (requireNotNull(tab)) {
|
||||
DisplayMode.SHOW_SONGS -> {
|
||||
updateSortMenu(sortItem, homeModel.songSortMode)
|
||||
updateSortMenu(sortItem, tab)
|
||||
|
||||
R.id.home_song_list
|
||||
}
|
||||
|
||||
DisplayMode.SHOW_ALBUMS -> {
|
||||
updateSortMenu(sortItem, homeModel.albumSortMode) { id ->
|
||||
updateSortMenu(sortItem, tab) { id ->
|
||||
id != R.id.option_sort_album
|
||||
}
|
||||
|
||||
|
@ -177,7 +177,7 @@ class HomeFragment : Fragment() {
|
|||
}
|
||||
|
||||
DisplayMode.SHOW_ARTISTS -> {
|
||||
updateSortMenu(sortItem, homeModel.artistSortMode) { id ->
|
||||
updateSortMenu(sortItem, tab) { id ->
|
||||
id == R.id.option_sort_asc || id == R.id.option_sort_dsc
|
||||
}
|
||||
|
||||
|
@ -185,7 +185,7 @@ class HomeFragment : Fragment() {
|
|||
}
|
||||
|
||||
DisplayMode.SHOW_GENRES -> {
|
||||
updateSortMenu(sortItem, homeModel.genreSortMode) { id ->
|
||||
updateSortMenu(sortItem, tab) { id ->
|
||||
id == R.id.option_sort_asc || id == R.id.option_sort_dsc
|
||||
}
|
||||
|
||||
|
@ -228,9 +228,11 @@ class HomeFragment : Fragment() {
|
|||
|
||||
private fun updateSortMenu(
|
||||
item: MenuItem,
|
||||
toHighlight: SortMode,
|
||||
displayMode: DisplayMode,
|
||||
isVisible: (Int) -> Boolean = { true }
|
||||
) {
|
||||
val toHighlight = homeModel.getSortForDisplay(displayMode)
|
||||
|
||||
for (option in item.subMenu) {
|
||||
if (option.itemId == toHighlight.itemId) {
|
||||
option.isChecked = true
|
||||
|
|
|
@ -26,6 +26,7 @@ import org.oxycblt.auxio.music.Artist
|
|||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.MusicStore
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.settings.SettingsManager
|
||||
import org.oxycblt.auxio.ui.DisplayMode
|
||||
import org.oxycblt.auxio.ui.SortMode
|
||||
|
||||
|
@ -56,19 +57,13 @@ class HomeViewModel : ViewModel() {
|
|||
private val mCurTab = MutableLiveData(mTabs.value!![0])
|
||||
val curTab: LiveData<DisplayMode> = mCurTab
|
||||
|
||||
var genreSortMode = SortMode.ASCENDING
|
||||
private set
|
||||
|
||||
var artistSortMode = SortMode.ASCENDING
|
||||
private set
|
||||
|
||||
var albumSortMode = SortMode.ASCENDING
|
||||
private set
|
||||
|
||||
var songSortMode = SortMode.ASCENDING
|
||||
private set
|
||||
private var genreSortMode = SortMode.ASCENDING
|
||||
private var artistSortMode = SortMode.ASCENDING
|
||||
private var albumSortMode = SortMode.ASCENDING
|
||||
private var songSortMode = SortMode.ASCENDING
|
||||
|
||||
private val musicStore = MusicStore.getInstance()
|
||||
private val settingsManager = SettingsManager.getInstance()
|
||||
|
||||
init {
|
||||
mSongs.value = songSortMode.sortSongs(musicStore.songs)
|
||||
|
@ -86,6 +81,15 @@ class HomeViewModel : ViewModel() {
|
|||
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].
|
||||
*/
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
|
||||
package org.oxycblt.auxio.music
|
||||
|
||||
import android.net.Uri
|
||||
import android.view.View
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
|
@ -87,7 +86,10 @@ data class Song(
|
|||
override val id: Long,
|
||||
override val name: String,
|
||||
val fileName: String,
|
||||
val albumName: String,
|
||||
val albumId: Long,
|
||||
val artistName: String,
|
||||
val year: Int,
|
||||
val track: Int,
|
||||
val duration: Long
|
||||
) : BaseModel(), Hashable {
|
||||
|
@ -97,8 +99,8 @@ data class Song(
|
|||
val genre: Genre? get() = mGenre
|
||||
val album: Album get() = requireNotNull(mAlbum)
|
||||
|
||||
val seconds = duration / 1000
|
||||
val formattedDuration = seconds.toDuration()
|
||||
val seconds: Long get() = duration / 1000
|
||||
val formattedDuration: String get() = (duration / 1000).toDuration()
|
||||
|
||||
override val hash: Int get() {
|
||||
var result = name.hashCode()
|
||||
|
@ -108,22 +110,17 @@ data class Song(
|
|||
}
|
||||
|
||||
fun linkAlbum(album: Album) {
|
||||
if (mAlbum == null) {
|
||||
mAlbum = album
|
||||
}
|
||||
mAlbum = album
|
||||
}
|
||||
|
||||
fun linkGenre(genre: Genre) {
|
||||
if (mGenre == null) {
|
||||
mGenre = genre
|
||||
}
|
||||
mGenre = genre
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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.
|
||||
|
@ -133,15 +130,18 @@ data class Album(
|
|||
override val id: Long,
|
||||
override val name: String,
|
||||
val artistName: String,
|
||||
val coverUri: Uri,
|
||||
val year: Int
|
||||
val year: Int,
|
||||
val songs: List<Song>
|
||||
) : Parent() {
|
||||
init {
|
||||
songs.forEach { song ->
|
||||
song.linkAlbum(this)
|
||||
}
|
||||
}
|
||||
|
||||
private var mArtist: Artist? = null
|
||||
val artist: Artist get() = requireNotNull(mArtist)
|
||||
|
||||
private val mSongs = mutableListOf<Song>()
|
||||
val songs: List<Song> get() = mSongs
|
||||
|
||||
val totalDuration: String get() =
|
||||
songs.sumOf { it.seconds }.toDuration()
|
||||
|
||||
|
@ -155,13 +155,6 @@ data class Album(
|
|||
fun linkArtist(artist: 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,
|
||||
val albums: List<Album>
|
||||
) : Parent() {
|
||||
init {
|
||||
albums.forEach { album ->
|
||||
album.linkArtist(this)
|
||||
}
|
||||
}
|
||||
|
||||
val genre: Genre? by lazy {
|
||||
// Get the genre that corresponds to the most songs in this artist, which would be
|
||||
// the most "Prominent" genre.
|
||||
|
@ -186,12 +185,6 @@ data class Artist(
|
|||
}
|
||||
|
||||
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.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.Media
|
||||
import androidx.core.database.getStringOrNull
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.excluded.ExcludedDatabase
|
||||
import org.oxycblt.auxio.ui.SortMode
|
||||
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
|
||||
*/
|
||||
class MusicLoader(private val context: Context) {
|
||||
|
@ -53,12 +109,11 @@ class MusicLoader(private val context: Context) {
|
|||
buildSelector()
|
||||
|
||||
loadGenres()
|
||||
loadAlbums()
|
||||
loadSongs()
|
||||
|
||||
linkAlbums()
|
||||
buildArtists()
|
||||
linkGenres()
|
||||
|
||||
buildAlbums()
|
||||
buildArtists()
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
|
@ -95,7 +150,8 @@ class MusicLoader(private val context: Context) {
|
|||
|
||||
while (cursor.moveToNext()) {
|
||||
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))
|
||||
}
|
||||
|
@ -104,53 +160,6 @@ class MusicLoader(private val context: Context) {
|
|||
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")
|
||||
private fun loadSongs() {
|
||||
logD("Starting song search...")
|
||||
|
@ -159,11 +168,15 @@ class MusicLoader(private val context: Context) {
|
|||
Media.EXTERNAL_CONTENT_URI,
|
||||
arrayOf(
|
||||
Media._ID, // 0
|
||||
Media.DISPLAY_NAME, // 1
|
||||
Media.TITLE, // 2
|
||||
Media.ALBUM_ID, // 3
|
||||
Media.TRACK, // 4
|
||||
Media.DURATION, // 5
|
||||
Media.TITLE, // 1
|
||||
Media.DISPLAY_NAME, // 2
|
||||
Media.ALBUM, // 3
|
||||
Media.ALBUM_ID, // 4
|
||||
Media.ARTIST, // 5
|
||||
Media.ALBUM_ARTIST, // 6
|
||||
Media.YEAR, // 7
|
||||
Media.TRACK, // 8
|
||||
Media.DURATION, // 9
|
||||
),
|
||||
selector, args,
|
||||
Media.DEFAULT_SORT_ORDER
|
||||
|
@ -173,7 +186,11 @@ class MusicLoader(private val context: Context) {
|
|||
val idIndex = cursor.getColumnIndexOrThrow(Media._ID)
|
||||
val titleIndex = cursor.getColumnIndexOrThrow(Media.TITLE)
|
||||
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 durationIndex = cursor.getColumnIndexOrThrow(Media.DURATION)
|
||||
|
||||
|
@ -181,111 +198,89 @@ class MusicLoader(private val context: Context) {
|
|||
val id = cursor.getLong(idIndex)
|
||||
val fileName = cursor.getString(fileIndex)
|
||||
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 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 {
|
||||
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()
|
||||
|
||||
logD("Song search finished with ${songs.size} found")
|
||||
}
|
||||
|
||||
private fun linkAlbums() {
|
||||
private fun buildAlbums() {
|
||||
logD("Linking albums")
|
||||
|
||||
// Group up songs by their album ids and then link them with their albums
|
||||
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 ->
|
||||
(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 = SortMode.ASCENDING.sortAlbums(albums).toMutableList()
|
||||
|
||||
// If something goes horribly wrong and somehow songs are still not linked up by the
|
||||
// album id, just throw them into an unknown album.
|
||||
if (unknownAlbum.songs.isNotEmpty()) {
|
||||
albums.add(unknownAlbum)
|
||||
}
|
||||
logD("Songs successfully linked into ${albums.size} albums")
|
||||
}
|
||||
|
||||
private fun buildArtists() {
|
||||
logD("Linking artists")
|
||||
|
||||
// Group albums up by their artist name, should not result in any null-artist issues
|
||||
val albumsByArtist = albums.groupBy { it.artistName }
|
||||
|
||||
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(
|
||||
// IDs are incremented from the minimum int value so that they remain unique.
|
||||
Artist(
|
||||
id = (Int.MIN_VALUE + artists.size).toLong(),
|
||||
id = entry.key.hashCode().toLong(),
|
||||
name = entry.key,
|
||||
albums = entry.value
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
artists = SortMode.ASCENDING.sortModels(artists).toMutableList()
|
||||
|
||||
logD("Albums successfully linked into ${artists.size} artists")
|
||||
}
|
||||
|
||||
private fun linkGenres() {
|
||||
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 ->
|
||||
val songCursor = resolver.query(
|
||||
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
|
||||
val songsWithoutGenres = songs.filter { it.genre == null }
|
||||
// Songs that don't have a genre will be thrown into an unknown genre.
|
||||
|
||||
if (songsWithoutGenres.isNotEmpty()) {
|
||||
val unknownGenre = Genre(
|
||||
id = -2,
|
||||
name = context.getString(R.string.def_genre)
|
||||
)
|
||||
val unknownGenre = Genre(
|
||||
id = -2,
|
||||
name = context.getString(R.string.def_genre)
|
||||
)
|
||||
|
||||
songsWithoutGenres.forEach { song ->
|
||||
songs.forEach { song ->
|
||||
if (song.genre == null) {
|
||||
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
|
||||
*/
|
||||
fun toInt(): Int {
|
||||
return when (this) {
|
||||
NONE -> CONST_NONE
|
||||
ALL -> CONST_ALL
|
||||
TRACK -> CONST_TRACK
|
||||
}
|
||||
return CONST_NONE + ordinal
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val CONST_NONE = 0xA100
|
||||
const val CONST_ALL = 0xA101
|
||||
const val CONST_TRACK = 0xA102
|
||||
private const val CONST_NONE = 0xA100
|
||||
private const val CONST_ALL = 0xA101
|
||||
private const val CONST_TRACK = 0xA102
|
||||
|
||||
/**
|
||||
* 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,
|
||||
*/
|
||||
fun toInt(): Int {
|
||||
return when (this) {
|
||||
IN_ARTIST -> CONST_IN_ARTIST
|
||||
IN_GENRE -> CONST_IN_GENRE
|
||||
IN_ALBUM -> CONST_IN_ALBUM
|
||||
ALL_SONGS -> CONST_ALL_SONGS
|
||||
}
|
||||
return CONST_IN_ARTIST + ordinal
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val CONST_IN_GENRE = 0xA103
|
||||
const val CONST_IN_ARTIST = 0xA104
|
||||
const val CONST_IN_ALBUM = 0xA105
|
||||
const val CONST_ALL_SONGS = 0xA106
|
||||
private const val CONST_IN_GENRE = 0xA103
|
||||
private const val CONST_IN_ARTIST = 0xA104
|
||||
private const val CONST_IN_ALBUM = 0xA105
|
||||
private const val CONST_ALL_SONGS = 0xA106
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
// Prevent ID collisions from occurring between datatypes.
|
||||
if (oldItem.javaClass != newItem.javaClass) return false
|
||||
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,11 @@ enum class DisplayMode {
|
|||
private const val CONST_SHOW_ALBUMS = 0xA10A
|
||||
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 {
|
||||
return when (value) {
|
||||
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? {
|
||||
return when (value) {
|
||||
CONST_SHOW_SONGS -> SHOW_SONGS
|
||||
|
|
Loading…
Reference in a new issue