Update music loading system

Do some simplification and optimizations to the music loading system in preperation for the blacklisting system.
This commit is contained in:
OxygenCobalt 2021-03-03 19:46:34 -07:00
parent c9f86436c8
commit a4dc35c50d
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
5 changed files with 122 additions and 161 deletions

View file

@ -15,6 +15,7 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/info_app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:exported="true"
android:supportsRtl="true"
android:theme="@style/Theme.Base">
<activity

View file

@ -1,7 +1,7 @@
package org.oxycblt.auxio.music.processing
package org.oxycblt.auxio.music
import android.annotation.SuppressLint
import android.app.Application
import android.content.Context
import android.provider.MediaStore
import android.provider.MediaStore.Audio.Albums
import android.provider.MediaStore.Audio.Genres
@ -9,30 +9,31 @@ 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.music.Album
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.toAlbumArtURI
/**
* Class that loads/constructs [Genre]s, [Album]s, and [Song] objects from the filesystem
* Artists are constructed in [MusicLinker], as they are only really containers for [Album]s
* Class that loads/constructs [Genre]s, [Artist]s, [Album]s, and [Song] objects from the filesystem
* @author OxygenCobalt
*/
class MusicLoader(private val app: Application) {
class MusicLoader(private val context: Context) {
var genres = mutableListOf<Genre>()
var albums = mutableListOf<Album>()
var artists = mutableListOf<Artist>()
var songs = mutableListOf<Song>()
private val resolver = app.contentResolver
private val resolver = context.contentResolver
/**
* Begin the loading process. Resulting models are pushed to [genres], [albums], and [songs].
* Begin the loading process.
* Resulting models are pushed to [genres], [artists], [albums], and [songs].
*/
fun loadMusic() {
fun load() {
loadGenres()
loadAlbums()
loadSongs()
linkAlbums()
buildArtists()
linkGenres()
}
private fun loadGenres() {
@ -60,14 +61,11 @@ class MusicLoader(private val app: Application) {
genres.add(Genre(id, name))
}
cursor.close()
}
logD("Genre search finished with ${genres.size} genres found.")
}
@SuppressLint("InlinedApi")
private fun loadAlbums() {
logD("Starting album search...")
@ -83,8 +81,8 @@ class MusicLoader(private val app: Application) {
Albums.DEFAULT_SORT_ORDER
)
val albumPlaceholder = app.getString(R.string.placeholder_album)
val artistPlaceholder = app.getString(R.string.placeholder_artist)
val albumPlaceholder = context.getString(R.string.placeholder_album)
val artistPlaceholder = context.getString(R.string.placeholder_artist)
albumCursor?.use { cursor ->
val idIndex = cursor.getColumnIndexOrThrow(Albums._ID)
@ -106,8 +104,6 @@ class MusicLoader(private val app: Application) {
albums.add(Album(id, name, artistName, coverUri, year))
}
cursor.close()
}
albums = albums.distinctBy {
@ -129,32 +125,30 @@ class MusicLoader(private val app: Application) {
Media.TITLE, // 2
Media.ALBUM_ID, // 3
Media.TRACK, // 4
Media.DURATION, // 5
Media.DURATION // 5
),
Media.IS_MUSIC + "=1", null,
"${Media.IS_MUSIC}=1", null,
Media.DEFAULT_SORT_ORDER
)
songCursor?.use { cursor ->
val idIndex = cursor.getColumnIndexOrThrow(Media._ID)
val fileIndex = cursor.getColumnIndexOrThrow(Media.DISPLAY_NAME)
val titleIndex = cursor.getColumnIndexOrThrow(Media.TITLE)
val fileIndex = cursor.getColumnIndexOrThrow(Media.DISPLAY_NAME)
val albumIndex = cursor.getColumnIndexOrThrow(Media.ALBUM_ID)
val trackIndex = cursor.getColumnIndexOrThrow(Media.TRACK)
val durationIndex = cursor.getColumnIndexOrThrow(Media.DURATION)
while (cursor.moveToNext()) {
val id = cursor.getLong(idIndex)
val title = cursor.getString(titleIndex)
val fileName = cursor.getString(fileIndex)
val title = cursor.getString(titleIndex) ?: fileName
val albumId = cursor.getLong(albumIndex)
val track = cursor.getInt(trackIndex)
val duration = cursor.getLong(durationIndex)
songs.add(Song(id, title ?: fileName, fileName, albumId, track, duration))
songs.add(Song(id, title, fileName, albumId, track, duration))
}
cursor.close()
}
songs = songs.distinctBy {
@ -163,4 +157,97 @@ class MusicLoader(private val app: Application) {
logD("Song search finished with ${songs.size} found")
}
private fun linkAlbums() {
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(
name = context.getString(R.string.placeholder_album),
artistName = context.getString(R.string.placeholder_artist)
)
songsByAlbum.forEach { entry ->
(albums.find { it.id == entry.key } ?: unknownAlbum).linkSongs(entry.value)
}
albums.removeAll { it.songs.isEmpty() }
// 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)
}
}
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 ->
artists.add(
// IDs are incremented from the minimum int value so that they remain unique.
Artist(
id = (artists.size + Int.MIN_VALUE).toLong(),
name = entry.key, albums = entry.value
)
)
}
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 an ideal world I should just be able to write MediaStore.Media.Audio.GENRE
* in the original song projection and then have it fetch the genre from the database, but
* no, why would ANYONE do that? Instead, I have to manually iterate through each genre, get
* A LIST OF SONGS FROM THEM, and then waste CPU cycles REPEATEDLY ITERATING through the
* songs list to LINK EACH SONG WITH THEIR GENRE. This is the bottleneck in my loader,
* without this code the load times drop from ~130ms to ~60ms, but of course I have to do
* this if I want an sensible genre system. Why is it this way? Nobody knows! Now this
* quirk is immortalized and has to be replicated in all future iterations of this API! Yay!
*
* I hate this platform so much.
*/
genres.forEach { genre ->
val songCursor = resolver.query(
Genres.Members.getContentUri("external", genre.id),
arrayOf(Genres.Members._ID),
null, null, null
)
songCursor?.use { cursor ->
val idIndex = cursor.getColumnIndexOrThrow(Genres.Members._ID)
while (cursor.moveToNext()) {
val id = cursor.getLong(idIndex)
songs.find { it.id == id }?.let { song ->
genre.linkSong(song)
}
}
}
}
// Any songs without genres will be thrown into an unknown genre
val songsWithoutGenres = songs.filter { it.genre == null }
if (songsWithoutGenres.isNotEmpty()) {
val unknownGenre = Genre(name = context.getString(R.string.placeholder_genre))
songsWithoutGenres.forEach { song ->
unknownGenre.linkSong(song)
}
genres.add(unknownGenre)
}
}
}

View file

@ -8,8 +8,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.oxycblt.auxio.logD
import org.oxycblt.auxio.logE
import org.oxycblt.auxio.music.processing.MusicLinker
import org.oxycblt.auxio.music.processing.MusicLoader
import java.lang.Exception
/**
@ -54,19 +52,16 @@ class MusicStore private constructor() {
try {
val loader = MusicLoader(app)
loader.loadMusic()
loader.load()
if (loader.songs.isEmpty()) {
return@withContext Response.NO_MUSIC
}
val linker = MusicLinker(app, loader.songs, loader.albums, loader.genres)
linker.link()
mSongs = linker.songs.toList()
mAlbums = linker.albums.toList()
mArtists = linker.artists.toList()
mGenres = linker.genres.toList()
mSongs = loader.songs
mAlbums = loader.albums
mArtists = loader.artists
mGenres = loader.genres
this@MusicStore.logD(
"Music load completed successfully in ${System.currentTimeMillis() - start}ms."

View file

@ -1,123 +0,0 @@
package org.oxycblt.auxio.music.processing
import android.content.Context
import android.provider.MediaStore.Audio.Genres
import org.oxycblt.auxio.R
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
/**
* Object that links music data, such as grouping songs into their albums & genres and creating
* artists out of the albums.
* @author OxygenCobalt
*/
class MusicLinker(
private val context: Context,
val songs: MutableList<Song>,
val albums: MutableList<Album>,
val genres: MutableList<Genre>
) {
private val resolver = context.contentResolver
val artists = mutableListOf<Artist>()
/**
* Begin the linking process.
* Modified models are pushed to [songs], [albums], [artists], and [genres]
*/
fun link() {
linkAlbums()
linkArtists()
linkGenres()
}
private fun linkAlbums() {
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(
name = context.getString(R.string.placeholder_album),
artistName = context.getString(R.string.placeholder_artist)
)
songsByAlbum.forEach { entry ->
(albums.find { it.id == entry.key } ?: unknownAlbum).linkSongs(entry.value)
}
// 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)
}
}
private fun linkArtists() {
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 ->
artists.add(
Artist(
id = (artists.size + Int.MIN_VALUE).toLong(),
name = entry.key, albums = entry.value
)
)
}
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 an ideal world I should just be able to write MediaStore.Media.Audio.GENRE
* in the original song projection and then have it fetch the genre from the database, but
* no, why would ANYONE do that? Instead, I have to manually iterate through each genre, get
* A LIST OF SONGS FROM THEM, and then waste CPU cycles REPEATEDLY ITERATING through the
* songs list to LINK EACH SONG WITH THEIR GENRE. Why is it this way? Nobody knows! Now this
* quirk is immortalized and has to be replicated in all future iterations of this API! Yay!
*
* I hate this platform so much.
*/
genres.forEach { genre ->
val songCursor = resolver.query(
Genres.Members.getContentUri("external", genre.id),
arrayOf(Genres.Members._ID),
null, null, null
)
songCursor?.use { cursor ->
val idIndex = cursor.getColumnIndexOrThrow(Genres.Members._ID)
while (cursor.moveToNext()) {
val id = cursor.getLong(idIndex)
songs.find { it.id == id }?.let { song ->
genre.linkSong(song)
}
}
}
}
// Any songs without genres will be thrown into an unknown genre
val songsWithoutGenres = songs.filter { it.genre == null }
if (songsWithoutGenres.isNotEmpty()) {
val unknownGenre = Genre(name = context.getString(R.string.placeholder_genre))
songsWithoutGenres.forEach { song ->
unknownGenre.linkSong(song)
}
genres.add(unknownGenre)
}
}
}

View file

@ -5,6 +5,7 @@ import android.graphics.drawable.GradientDrawable
import android.os.Build
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
@ -66,11 +67,11 @@ class CobaltScrollThumb @JvmOverloads constructor(
}
}
isVisible = false
visibility = View.INVISIBLE
isActivated = false
post {
isVisible = true
visibility = View.VISIBLE
}
}