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:
parent
c9f86436c8
commit
a4dc35c50d
5 changed files with 122 additions and 161 deletions
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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."
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue