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:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/info_app_name"
|
android:label="@string/info_app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:exported="true"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.Base">
|
android:theme="@style/Theme.Base">
|
||||||
<activity
|
<activity
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package org.oxycblt.auxio.music.processing
|
package org.oxycblt.auxio.music
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Application
|
import android.content.Context
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.provider.MediaStore.Audio.Albums
|
import android.provider.MediaStore.Audio.Albums
|
||||||
import android.provider.MediaStore.Audio.Genres
|
import android.provider.MediaStore.Audio.Genres
|
||||||
|
@ -9,30 +9,31 @@ 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.logD
|
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
|
* Class that loads/constructs [Genre]s, [Artist]s, [Album]s, and [Song] objects from the filesystem
|
||||||
* Artists are constructed in [MusicLinker], as they are only really containers for [Album]s
|
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class MusicLoader(private val app: Application) {
|
class MusicLoader(private val context: Context) {
|
||||||
var genres = mutableListOf<Genre>()
|
var genres = mutableListOf<Genre>()
|
||||||
var albums = mutableListOf<Album>()
|
var albums = mutableListOf<Album>()
|
||||||
|
var artists = mutableListOf<Artist>()
|
||||||
var songs = mutableListOf<Song>()
|
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()
|
loadGenres()
|
||||||
loadAlbums()
|
loadAlbums()
|
||||||
loadSongs()
|
loadSongs()
|
||||||
|
|
||||||
|
linkAlbums()
|
||||||
|
buildArtists()
|
||||||
|
linkGenres()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadGenres() {
|
private fun loadGenres() {
|
||||||
|
@ -60,14 +61,11 @@ class MusicLoader(private val app: Application) {
|
||||||
|
|
||||||
genres.add(Genre(id, name))
|
genres.add(Genre(id, name))
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor.close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logD("Genre search finished with ${genres.size} genres found.")
|
logD("Genre search finished with ${genres.size} genres found.")
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
|
||||||
private fun loadAlbums() {
|
private fun loadAlbums() {
|
||||||
logD("Starting album search...")
|
logD("Starting album search...")
|
||||||
|
|
||||||
|
@ -83,8 +81,8 @@ class MusicLoader(private val app: Application) {
|
||||||
Albums.DEFAULT_SORT_ORDER
|
Albums.DEFAULT_SORT_ORDER
|
||||||
)
|
)
|
||||||
|
|
||||||
val albumPlaceholder = app.getString(R.string.placeholder_album)
|
val albumPlaceholder = context.getString(R.string.placeholder_album)
|
||||||
val artistPlaceholder = app.getString(R.string.placeholder_artist)
|
val artistPlaceholder = context.getString(R.string.placeholder_artist)
|
||||||
|
|
||||||
albumCursor?.use { cursor ->
|
albumCursor?.use { cursor ->
|
||||||
val idIndex = cursor.getColumnIndexOrThrow(Albums._ID)
|
val idIndex = cursor.getColumnIndexOrThrow(Albums._ID)
|
||||||
|
@ -106,8 +104,6 @@ class MusicLoader(private val app: Application) {
|
||||||
|
|
||||||
albums.add(Album(id, name, artistName, coverUri, year))
|
albums.add(Album(id, name, artistName, coverUri, year))
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor.close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
albums = albums.distinctBy {
|
albums = albums.distinctBy {
|
||||||
|
@ -129,32 +125,30 @@ class MusicLoader(private val app: Application) {
|
||||||
Media.TITLE, // 2
|
Media.TITLE, // 2
|
||||||
Media.ALBUM_ID, // 3
|
Media.ALBUM_ID, // 3
|
||||||
Media.TRACK, // 4
|
Media.TRACK, // 4
|
||||||
Media.DURATION, // 5
|
Media.DURATION // 5
|
||||||
),
|
),
|
||||||
Media.IS_MUSIC + "=1", null,
|
"${Media.IS_MUSIC}=1", null,
|
||||||
Media.DEFAULT_SORT_ORDER
|
Media.DEFAULT_SORT_ORDER
|
||||||
)
|
)
|
||||||
|
|
||||||
songCursor?.use { cursor ->
|
songCursor?.use { cursor ->
|
||||||
val idIndex = cursor.getColumnIndexOrThrow(Media._ID)
|
val idIndex = cursor.getColumnIndexOrThrow(Media._ID)
|
||||||
val fileIndex = cursor.getColumnIndexOrThrow(Media.DISPLAY_NAME)
|
|
||||||
val titleIndex = cursor.getColumnIndexOrThrow(Media.TITLE)
|
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_ID)
|
||||||
val trackIndex = cursor.getColumnIndexOrThrow(Media.TRACK)
|
val trackIndex = cursor.getColumnIndexOrThrow(Media.TRACK)
|
||||||
val durationIndex = cursor.getColumnIndexOrThrow(Media.DURATION)
|
val durationIndex = cursor.getColumnIndexOrThrow(Media.DURATION)
|
||||||
|
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
val id = cursor.getLong(idIndex)
|
val id = cursor.getLong(idIndex)
|
||||||
val title = cursor.getString(titleIndex)
|
|
||||||
val fileName = cursor.getString(fileIndex)
|
val fileName = cursor.getString(fileIndex)
|
||||||
|
val title = cursor.getString(titleIndex) ?: fileName
|
||||||
val albumId = cursor.getLong(albumIndex)
|
val albumId = cursor.getLong(albumIndex)
|
||||||
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, fileName, albumId, track, duration))
|
songs.add(Song(id, title, fileName, albumId, track, duration))
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor.close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
songs = songs.distinctBy {
|
songs = songs.distinctBy {
|
||||||
|
@ -163,4 +157,97 @@ class MusicLoader(private val app: Application) {
|
||||||
|
|
||||||
logD("Song search finished with ${songs.size} found")
|
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 kotlinx.coroutines.withContext
|
||||||
import org.oxycblt.auxio.logD
|
import org.oxycblt.auxio.logD
|
||||||
import org.oxycblt.auxio.logE
|
import org.oxycblt.auxio.logE
|
||||||
import org.oxycblt.auxio.music.processing.MusicLinker
|
|
||||||
import org.oxycblt.auxio.music.processing.MusicLoader
|
|
||||||
import java.lang.Exception
|
import java.lang.Exception
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -54,19 +52,16 @@ class MusicStore private constructor() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val loader = MusicLoader(app)
|
val loader = MusicLoader(app)
|
||||||
loader.loadMusic()
|
loader.load()
|
||||||
|
|
||||||
if (loader.songs.isEmpty()) {
|
if (loader.songs.isEmpty()) {
|
||||||
return@withContext Response.NO_MUSIC
|
return@withContext Response.NO_MUSIC
|
||||||
}
|
}
|
||||||
|
|
||||||
val linker = MusicLinker(app, loader.songs, loader.albums, loader.genres)
|
mSongs = loader.songs
|
||||||
linker.link()
|
mAlbums = loader.albums
|
||||||
|
mArtists = loader.artists
|
||||||
mSongs = linker.songs.toList()
|
mGenres = loader.genres
|
||||||
mAlbums = linker.albums.toList()
|
|
||||||
mArtists = linker.artists.toList()
|
|
||||||
mGenres = linker.genres.toList()
|
|
||||||
|
|
||||||
this@MusicStore.logD(
|
this@MusicStore.logD(
|
||||||
"Music load completed successfully in ${System.currentTimeMillis() - start}ms."
|
"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.os.Build
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
@ -66,11 +67,11 @@ class CobaltScrollThumb @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isVisible = false
|
visibility = View.INVISIBLE
|
||||||
isActivated = false
|
isActivated = false
|
||||||
|
|
||||||
post {
|
post {
|
||||||
isVisible = true
|
visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue