music: clean up loader implementation
Clean up the music loader implementation, removing pre-sorting to make it a bit more efficent. Instead, sorting is done on indiviual components.
This commit is contained in:
parent
cde3a99f4d
commit
637bcccd51
12 changed files with 155 additions and 191 deletions
|
@ -33,6 +33,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.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
import org.oxycblt.auxio.ui.Sort
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -76,7 +77,10 @@ class ArtistImageFetcher private constructor(
|
||||||
private val artist: Artist,
|
private val artist: Artist,
|
||||||
) : AuxioFetcher() {
|
) : AuxioFetcher() {
|
||||||
override suspend fun fetch(): FetchResult? {
|
override suspend fun fetch(): FetchResult? {
|
||||||
val results = artist.albums.mapAtMost(4) { album ->
|
val albums = Sort.ByName(true)
|
||||||
|
.sortAlbums(artist.albums)
|
||||||
|
|
||||||
|
val results = albums.mapAtMost(4) { album ->
|
||||||
fetchArt(context, album)
|
fetchArt(context, album)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,6 +104,7 @@ class GenreImageFetcher private constructor(
|
||||||
private val genre: Genre,
|
private val genre: Genre,
|
||||||
) : AuxioFetcher() {
|
) : AuxioFetcher() {
|
||||||
override suspend fun fetch(): FetchResult? {
|
override suspend fun fetch(): FetchResult? {
|
||||||
|
// We don't need to sort here, as the way we
|
||||||
val albums = genre.songs.groupBy { it.album }.keys
|
val albums = genre.songs.groupBy { it.album }.keys
|
||||||
val results = albums.mapAtMost(4) { album ->
|
val results = albums.mapAtMost(4) { album ->
|
||||||
fetchArt(context, album)
|
fetchArt(context, album)
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.excluded
|
package org.oxycblt.auxio.excluded
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
|
@ -32,14 +31,13 @@ import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.MainActivity
|
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.DialogExcludedBinding
|
import org.oxycblt.auxio.databinding.DialogExcludedBinding
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.ui.LifecycleDialog
|
import org.oxycblt.auxio.ui.LifecycleDialog
|
||||||
|
import org.oxycblt.auxio.util.hardRestart
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.showToast
|
import org.oxycblt.auxio.util.showToast
|
||||||
import kotlin.system.exitProcess
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dialog that manages the currently excluded directories.
|
* Dialog that manages the currently excluded directories.
|
||||||
|
@ -150,25 +148,12 @@ class ExcludedDialog : LifecycleDialog() {
|
||||||
|
|
||||||
private fun saveAndRestart() {
|
private fun saveAndRestart() {
|
||||||
excludedModel.save {
|
excludedModel.save {
|
||||||
playbackModel.savePlaybackState(requireContext(), ::hardRestart)
|
playbackModel.savePlaybackState(requireContext()) {
|
||||||
|
requireContext().hardRestart()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hardRestart() {
|
|
||||||
logD("Performing hard restart.")
|
|
||||||
|
|
||||||
// Instead of having to do a ton of cleanup and horrible code changes
|
|
||||||
// to restart this application non-destructively, I just restart the UI task [There is only
|
|
||||||
// one, after all] and then kill the application using exitProcess. Works well enough.
|
|
||||||
val intent = Intent(requireContext().applicationContext, MainActivity::class.java).setFlags(
|
|
||||||
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
|
||||||
)
|
|
||||||
|
|
||||||
startActivity(intent)
|
|
||||||
|
|
||||||
exitProcess(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get *just* the root path, nothing else is really needed.
|
* Get *just* the root path, nothing else is really needed.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,37 +1,14 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2021 Auxio Project
|
|
||||||
* MusicLoader.kt is part of Auxio.
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.oxycblt.auxio.music
|
package org.oxycblt.auxio.music
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.provider.MediaStore.Audio.Genres
|
|
||||||
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.Sort
|
|
||||||
import org.oxycblt.auxio.util.logD
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class does pretty much all the black magic required to get a remotely sensible music
|
* This class acts as the base for most 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
|
* indexing system while still optimizing for time. I would recommend you leave this module now
|
||||||
* before you lose your sanity trying to understand the hoops I had to jump through for this
|
* 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.
|
* system, but if you really want to stay, here's a debrief on why this code is so awful.
|
||||||
*
|
*
|
||||||
|
@ -90,111 +67,72 @@ import org.oxycblt.auxio.util.logD
|
||||||
*
|
*
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class MusicLoader(private val context: Context) {
|
class MusicLoader {
|
||||||
var genres = mutableListOf<Genre>()
|
data class Library(
|
||||||
var albums = mutableListOf<Album>()
|
val genres: List<Genre>,
|
||||||
var artists = mutableListOf<Artist>()
|
val artists: List<Artist>,
|
||||||
var songs = mutableListOf<Song>()
|
val albums: List<Album>,
|
||||||
|
val songs: List<Song>
|
||||||
|
)
|
||||||
|
|
||||||
private val resolver = context.contentResolver
|
fun load(context: Context): Library? {
|
||||||
|
val songs = loadSongs(context)
|
||||||
|
if (songs.isEmpty()) return null
|
||||||
|
|
||||||
private var selector = "${Media.IS_MUSIC}=1"
|
val albums = buildAlbums(songs)
|
||||||
private var args = arrayOf<String>()
|
val artists = buildArtists(context, albums)
|
||||||
|
val genres = readGenres(context, songs)
|
||||||
|
|
||||||
/**
|
return Library(
|
||||||
* Begin the loading process.
|
genres,
|
||||||
* Resulting models are pushed to [genres], [artists], [albums], and [songs].
|
artists,
|
||||||
*/
|
albums,
|
||||||
fun load() {
|
songs
|
||||||
buildSelector()
|
)
|
||||||
|
|
||||||
loadGenres()
|
|
||||||
loadSongs()
|
|
||||||
linkGenres()
|
|
||||||
|
|
||||||
buildAlbums()
|
|
||||||
buildArtists()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
private fun loadSongs(context: Context): List<Song> {
|
||||||
private fun buildSelector() {
|
var songs = mutableListOf<Song>()
|
||||||
val blacklistDatabase = ExcludedDatabase.getInstance(context)
|
val blacklistDatabase = ExcludedDatabase.getInstance(context)
|
||||||
|
|
||||||
val paths = blacklistDatabase.readPaths()
|
val paths = blacklistDatabase.readPaths()
|
||||||
|
|
||||||
// DATA was deprecated on Android Q, but is set to be un-deprecated in Android 12L
|
var selector = "${MediaStore.Audio.Media.IS_MUSIC}=1"
|
||||||
|
val args = mutableListOf<String>()
|
||||||
|
|
||||||
|
// DATA was deprecated on Android 10, but is set to be un-deprecated in Android 12L.
|
||||||
// The only reason we'd want to change this is to add external partitions support, but
|
// The only reason we'd want to change this is to add external partitions support, but
|
||||||
// that's less efficent and there's no demand for that right now.
|
// that's less efficent and there's no demand for that right now.
|
||||||
for (path in paths) {
|
for (path in paths) {
|
||||||
selector += " AND ${Media.DATA} NOT LIKE ?"
|
selector += " AND ${MediaStore.Audio.Media.DATA} NOT LIKE ?"
|
||||||
args += "$path%" // Append % so that the selector properly detects children
|
args += "$path%" // Append % so that the selector properly detects children
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadGenres() {
|
context.contentResolver.query(
|
||||||
logD("Starting genre search...")
|
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
||||||
|
|
||||||
// First, get a cursor for every genre in the android system
|
|
||||||
val genreCursor = resolver.query(
|
|
||||||
Genres.EXTERNAL_CONTENT_URI,
|
|
||||||
arrayOf(
|
arrayOf(
|
||||||
Genres._ID, // 0
|
MediaStore.Audio.Media._ID,
|
||||||
Genres.NAME // 1
|
MediaStore.Audio.Media.TITLE,
|
||||||
|
MediaStore.Audio.Media.DISPLAY_NAME,
|
||||||
|
MediaStore.Audio.Media.ALBUM,
|
||||||
|
MediaStore.Audio.Media.ALBUM_ID,
|
||||||
|
MediaStore.Audio.Media.ARTIST,
|
||||||
|
MediaStore.Audio.Media.ALBUM_ARTIST,
|
||||||
|
MediaStore.Audio.Media.YEAR,
|
||||||
|
MediaStore.Audio.Media.TRACK,
|
||||||
|
MediaStore.Audio.Media.DURATION,
|
||||||
),
|
),
|
||||||
null, null,
|
selector, args.toTypedArray(), null
|
||||||
Genres.DEFAULT_SORT_ORDER
|
)?.use { cursor ->
|
||||||
)
|
val idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID)
|
||||||
|
val titleIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE)
|
||||||
// And then process those into Genre objects
|
val fileIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME)
|
||||||
genreCursor?.use { cursor ->
|
val albumIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM)
|
||||||
val idIndex = cursor.getColumnIndexOrThrow(Genres._ID)
|
val albumIdIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID)
|
||||||
val nameIndex = cursor.getColumnIndexOrThrow(Genres.NAME)
|
val artistIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST)
|
||||||
|
val albumArtistIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ARTIST)
|
||||||
while (cursor.moveToNext()) {
|
val yearIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.YEAR)
|
||||||
val id = cursor.getLong(idIndex)
|
val trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TRACK)
|
||||||
// No non-broken genre would be missing a name.
|
val durationIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION)
|
||||||
val name = cursor.getStringOrNull(nameIndex) ?: continue
|
|
||||||
|
|
||||||
genres.add(Genre(id, name, name.getGenreNameCompat() ?: name))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logD("Genre search finished with ${genres.size} genres found.")
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
|
||||||
private fun loadSongs() {
|
|
||||||
logD("Starting song search...")
|
|
||||||
|
|
||||||
val songCursor = resolver.query(
|
|
||||||
Media.EXTERNAL_CONTENT_URI,
|
|
||||||
arrayOf(
|
|
||||||
Media._ID, // 0
|
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
songCursor?.use { cursor ->
|
|
||||||
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)
|
|
||||||
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)
|
|
||||||
|
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
val id = cursor.getLong(idIndex)
|
val id = cursor.getLong(idIndex)
|
||||||
|
@ -226,20 +164,19 @@ class MusicLoader(private val context: Context) {
|
||||||
it.name to it.albumName to it.artistName to it.track to it.duration
|
it.name to it.albumName to it.artistName to it.track to it.duration
|
||||||
}.toMutableList()
|
}.toMutableList()
|
||||||
|
|
||||||
logD("Song search finished with ${songs.size} found")
|
return songs
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildAlbums() {
|
private fun buildAlbums(songs: List<Song>): List<Album> {
|
||||||
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 albums = mutableListOf<Album>()
|
||||||
val songsByAlbum = songs.groupBy { it.albumId }
|
val songsByAlbum = songs.groupBy { it.albumId }
|
||||||
|
|
||||||
songsByAlbum.forEach { entry ->
|
songsByAlbum.forEach { entry ->
|
||||||
// Rely on the first song in this list for album information.
|
// Use the song with the latest year as our metadata song.
|
||||||
// Note: This might result in a bad year being used for an album if an album's songs
|
// This allows us to replicate the LAST_YEAR field, which is useful as it means that
|
||||||
// have multiple years. This is fixable but is currently omitted for speed.
|
// weird years like "0" wont show up if there are alternatives.
|
||||||
val song = entry.value[0]
|
val song = requireNotNull(entry.value.maxByOrNull { it.year })
|
||||||
|
|
||||||
albums.add(
|
albums.add(
|
||||||
Album(
|
Album(
|
||||||
|
@ -253,14 +190,12 @@ class MusicLoader(private val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
albums.removeAll { it.songs.isEmpty() }
|
albums.removeAll { it.songs.isEmpty() }
|
||||||
albums = Sort.ByName(true).sortAlbums(albums).toMutableList()
|
|
||||||
|
|
||||||
logD("Songs successfully linked into ${albums.size} albums")
|
return albums
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildArtists() {
|
private fun buildArtists(context: Context, albums: List<Album>): List<Artist> {
|
||||||
logD("Linking artists")
|
val artists = mutableListOf<Artist>()
|
||||||
|
|
||||||
val albumsByArtist = albums.groupBy { it.artistName }
|
val albumsByArtist = albums.groupBy { it.artistName }
|
||||||
|
|
||||||
albumsByArtist.forEach { entry ->
|
albumsByArtist.forEach { entry ->
|
||||||
|
@ -270,8 +205,8 @@ class MusicLoader(private val context: Context) {
|
||||||
entry.key
|
entry.key
|
||||||
}
|
}
|
||||||
|
|
||||||
// Because of our hacky album artist system, MediaStore artist IDs are unreliable.
|
// In most cases, MediaStore artist IDs are unreliable or omitted for speed.
|
||||||
// Therefore we just use the hashCode of the artist name as our ID and move on.
|
// Use the hashCode of the artist name as our ID and move on.
|
||||||
artists.add(
|
artists.add(
|
||||||
Artist(
|
Artist(
|
||||||
id = entry.key.hashCode().toLong(),
|
id = entry.key.hashCode().toLong(),
|
||||||
|
@ -282,36 +217,42 @@ class MusicLoader(private val context: Context) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
artists = Sort.ByName(true).sortParents(artists).toMutableList()
|
return artists
|
||||||
|
|
||||||
logD("Albums successfully linked into ${artists.size} artists")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun linkGenres() {
|
private fun readGenres(context: Context, songs: List<Song>): List<Genre> {
|
||||||
logD("Linking genres")
|
val genres = mutableListOf<Genre>()
|
||||||
|
|
||||||
genres.forEach { genre ->
|
// First, get a cursor for every genre in the android system
|
||||||
val songCursor = resolver.query(
|
val genreCursor = context.contentResolver.query(
|
||||||
Genres.Members.getContentUri("external", genre.id),
|
MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI,
|
||||||
arrayOf(Genres.Members._ID),
|
arrayOf(
|
||||||
null, null, null // Dont even bother blacklisting here as useless iters are less expensive than IO
|
MediaStore.Audio.Genres._ID,
|
||||||
)
|
MediaStore.Audio.Genres.NAME
|
||||||
|
),
|
||||||
|
null, null, null
|
||||||
|
)
|
||||||
|
|
||||||
songCursor?.use { cursor ->
|
// And then process those into Genre objects
|
||||||
val idIndex = cursor.getColumnIndexOrThrow(Genres.Members._ID)
|
genreCursor?.use { cursor ->
|
||||||
|
val idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres._ID)
|
||||||
|
val nameIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.NAME)
|
||||||
|
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
val id = cursor.getLong(idIndex)
|
// No non-broken genre would be missing a name.
|
||||||
|
val id = cursor.getLong(idIndex)
|
||||||
|
val name = cursor.getStringOrNull(nameIndex) ?: continue
|
||||||
|
|
||||||
songs.find { it.id == id }?.let { song ->
|
val genre = Genre(
|
||||||
genre.linkSong(song)
|
id, name, name.getGenreNameCompat() ?: name
|
||||||
}
|
)
|
||||||
}
|
|
||||||
|
linkGenre(context, genre, songs)
|
||||||
|
genres.add(genre)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Songs that don't have a genre will be thrown into an unknown genre.
|
// Songs that don't have a genre will be thrown into an unknown genre.
|
||||||
|
|
||||||
val unknownGenre = Genre(
|
val unknownGenre = Genre(
|
||||||
id = Long.MIN_VALUE,
|
id = Long.MIN_VALUE,
|
||||||
name = MediaStore.UNKNOWN_STRING,
|
name = MediaStore.UNKNOWN_STRING,
|
||||||
|
@ -327,5 +268,27 @@ class MusicLoader(private val context: Context) {
|
||||||
if (unknownGenre.songs.isNotEmpty()) {
|
if (unknownGenre.songs.isNotEmpty()) {
|
||||||
genres.add(unknownGenre)
|
genres.add(unknownGenre)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return genres
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun linkGenre(context: Context, genre: Genre, songs: List<Song>) {
|
||||||
|
val songCursor = context.contentResolver.query(
|
||||||
|
MediaStore.Audio.Genres.Members.getContentUri("external", genre.id),
|
||||||
|
arrayOf(MediaStore.Audio.Genres.Members._ID),
|
||||||
|
null, null, null // Dont even bother blacklisting here as useless iters are less expensive than IO
|
||||||
|
)
|
||||||
|
|
||||||
|
songCursor?.use { cursor ->
|
||||||
|
val idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.Members._ID)
|
||||||
|
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
val id = cursor.getLong(idIndex)
|
||||||
|
|
||||||
|
songs.find { it.id == id }?.let { song ->
|
||||||
|
genre.linkSong(song)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,17 +68,13 @@ class MusicStore private constructor() {
|
||||||
try {
|
try {
|
||||||
val start = System.currentTimeMillis()
|
val start = System.currentTimeMillis()
|
||||||
|
|
||||||
val loader = MusicLoader(context)
|
val loader = MusicLoader()
|
||||||
loader.load()
|
val library = loader.load(context) ?: return Response.Err(ErrorKind.NO_MUSIC)
|
||||||
|
|
||||||
if (loader.songs.isEmpty()) {
|
mSongs = library.songs
|
||||||
return Response.Err(ErrorKind.NO_MUSIC)
|
mAlbums = library.albums
|
||||||
}
|
mArtists = library.artists
|
||||||
|
mGenres = library.genres
|
||||||
mSongs = loader.songs
|
|
||||||
mAlbums = loader.albums
|
|
||||||
mArtists = loader.artists
|
|
||||||
mGenres = loader.genres
|
|
||||||
|
|
||||||
logD("Music load completed successfully in ${System.currentTimeMillis() - start}ms.")
|
logD("Music load completed successfully in ${System.currentTimeMillis() - start}ms.")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
|
@ -33,6 +33,7 @@ import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
import org.oxycblt.auxio.music.MusicStore
|
||||||
import org.oxycblt.auxio.settings.SettingsManager
|
import org.oxycblt.auxio.settings.SettingsManager
|
||||||
import org.oxycblt.auxio.ui.DisplayMode
|
import org.oxycblt.auxio.ui.DisplayMode
|
||||||
|
import org.oxycblt.auxio.ui.Sort
|
||||||
import java.text.Normalizer
|
import java.text.Normalizer
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -77,6 +78,7 @@ class SearchViewModel : ViewModel() {
|
||||||
|
|
||||||
// Searching can be quite expensive, so hop on a co-routine
|
// Searching can be quite expensive, so hop on a co-routine
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
val sort = Sort.ByName(true)
|
||||||
val results = mutableListOf<BaseModel>()
|
val results = mutableListOf<BaseModel>()
|
||||||
|
|
||||||
// A filter mode of null means to not filter at all.
|
// A filter mode of null means to not filter at all.
|
||||||
|
@ -84,28 +86,28 @@ class SearchViewModel : ViewModel() {
|
||||||
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_ARTISTS) {
|
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_ARTISTS) {
|
||||||
musicStore.artists.filterByOrNull(query)?.let { artists ->
|
musicStore.artists.filterByOrNull(query)?.let { artists ->
|
||||||
results.add(Header(-1, HeaderString.Single(R.string.lbl_artists)))
|
results.add(Header(-1, HeaderString.Single(R.string.lbl_artists)))
|
||||||
results.addAll(artists)
|
results.addAll(sort.sortParents(artists))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_ALBUMS) {
|
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_ALBUMS) {
|
||||||
musicStore.albums.filterByOrNull(query)?.let { albums ->
|
musicStore.albums.filterByOrNull(query)?.let { albums ->
|
||||||
results.add(Header(-2, HeaderString.Single(R.string.lbl_albums)))
|
results.add(Header(-2, HeaderString.Single(R.string.lbl_albums)))
|
||||||
results.addAll(albums)
|
results.addAll(sort.sortAlbums(albums))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_GENRES) {
|
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_GENRES) {
|
||||||
musicStore.genres.filterByOrNull(query)?.let { genres ->
|
musicStore.genres.filterByOrNull(query)?.let { genres ->
|
||||||
results.add(Header(-3, HeaderString.Single(R.string.lbl_genres)))
|
results.add(Header(-3, HeaderString.Single(R.string.lbl_genres)))
|
||||||
results.addAll(genres)
|
results.addAll(sort.sortParents(genres))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_SONGS) {
|
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_SONGS) {
|
||||||
musicStore.songs.filterByOrNull(query)?.let { songs ->
|
musicStore.songs.filterByOrNull(query)?.let { songs ->
|
||||||
results.add(Header(-4, HeaderString.Single(R.string.lbl_songs)))
|
results.add(Header(-4, HeaderString.Single(R.string.lbl_songs)))
|
||||||
results.addAll(songs)
|
results.addAll(sort.sortSongs(songs))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,7 +138,7 @@ class SearchViewModel : ViewModel() {
|
||||||
* Shortcut that will run a ignoreCase filter on a list and only return
|
* Shortcut that will run a ignoreCase filter on a list and only return
|
||||||
* a value if the resulting list is empty.
|
* a value if the resulting list is empty.
|
||||||
*/
|
*/
|
||||||
private fun List<Music>.filterByOrNull(value: String): List<BaseModel>? {
|
private fun <T : Music> List<T>.filterByOrNull(value: String): List<T>? {
|
||||||
val filtered = filter {
|
val filtered = filter {
|
||||||
// Ensure the name we match with is correct.
|
// Ensure the name we match with is correct.
|
||||||
val name = if (it is MusicParent) {
|
val name = if (it is MusicParent) {
|
||||||
|
|
|
@ -30,6 +30,7 @@ import androidx.annotation.StringRes
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import org.oxycblt.auxio.MainActivity
|
import org.oxycblt.auxio.MainActivity
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
const val INTENT_REQUEST_CODE = 0xA0A0
|
const val INTENT_REQUEST_CODE = 0xA0A0
|
||||||
|
|
||||||
|
@ -83,6 +84,19 @@ fun Context.newMainIntent(): PendingIntent {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Context.hardRestart() {
|
||||||
|
// Instead of having to do a ton of cleanup and horrible code changes
|
||||||
|
// to restart this application non-destructively, I just restart the UI task [There is only
|
||||||
|
// one, after all] and then kill the application using exitProcess. Works well enough.
|
||||||
|
val intent = Intent(applicationContext, MainActivity::class.java).setFlags(
|
||||||
|
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
)
|
||||||
|
|
||||||
|
startActivity(intent)
|
||||||
|
|
||||||
|
exitProcess(0)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a toast using the provided string resource.
|
* Create a toast using the provided string resource.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -55,7 +55,7 @@
|
||||||
<string name="set_theme_night">"Tmavé"</string>
|
<string name="set_theme_night">"Tmavé"</string>
|
||||||
<string name="set_accent">"Barva"</string>
|
<string name="set_accent">"Barva"</string>
|
||||||
<string name="set_black_mode">"Černé téma"</string>
|
<string name="set_black_mode">"Černé téma"</string>
|
||||||
<string name="setting_black_mode_desc">"Použít kompletně černé tmavé téma"</string>
|
<string name="set_black_mode_desc">"Použít kompletně černé tmavé téma"</string>
|
||||||
<string name="set_display">"Zobrazení"</string>
|
<string name="set_display">"Zobrazení"</string>
|
||||||
<string name="set_show_covers">"Zobrazit barvy alba"</string>
|
<string name="set_show_covers">"Zobrazit barvy alba"</string>
|
||||||
<string name="set_show_covers_desc">"Vypněte pro ušetření paměti"</string>
|
<string name="set_show_covers_desc">"Vypněte pro ušetření paměti"</string>
|
||||||
|
|
|
@ -154,7 +154,7 @@
|
||||||
<string name="lbl_sort_album">Album</string>
|
<string name="lbl_sort_album">Album</string>
|
||||||
<string name="lbl_sort_year">Jahr</string>
|
<string name="lbl_sort_year">Jahr</string>
|
||||||
<string name="set_black_mode">Schwarzes Thema</string>
|
<string name="set_black_mode">Schwarzes Thema</string>
|
||||||
<string name="setting_black_mode_desc">Ein schwarzes Thema für das dunkle verwenden</string>
|
<string name="set_black_mode_desc">Ein schwarzes Thema für das dunkle verwenden</string>
|
||||||
<string name="set_loop_pause">Pause bei Wiederholung</string>
|
<string name="set_loop_pause">Pause bei Wiederholung</string>
|
||||||
<string name="set_loop_pause_desc">Pausiert, wenn ein Song wiederholt wird</string>
|
<string name="set_loop_pause_desc">Pausiert, wenn ein Song wiederholt wird</string>
|
||||||
<string name="desc_shuffle">Zufällig an- oder ausschalten</string>
|
<string name="desc_shuffle">Zufällig an- oder ausschalten</string>
|
||||||
|
|
|
@ -60,7 +60,7 @@
|
||||||
<string name="set_theme_night">Oscuro</string>
|
<string name="set_theme_night">Oscuro</string>
|
||||||
<string name="set_accent">Acento</string>
|
<string name="set_accent">Acento</string>
|
||||||
<string name="set_black_mode">Tema negro</string>
|
<string name="set_black_mode">Tema negro</string>
|
||||||
<string name="setting_black_mode_desc">Usar tema negro puro</string>
|
<string name="set_black_mode_desc">Usar tema negro puro</string>
|
||||||
|
|
||||||
<string name="set_display">Pantalla</string>
|
<string name="set_display">Pantalla</string>
|
||||||
<string name="set_show_covers">Mostrar carátula de álbum</string>
|
<string name="set_show_covers">Mostrar carátula de álbum</string>
|
||||||
|
|
|
@ -65,7 +65,7 @@
|
||||||
<string name="set_theme_night">Dark</string>
|
<string name="set_theme_night">Dark</string>
|
||||||
<string name="set_accent">Color scheme</string>
|
<string name="set_accent">Color scheme</string>
|
||||||
<string name="set_black_mode">Black theme</string>
|
<string name="set_black_mode">Black theme</string>
|
||||||
<string name="setting_black_mode_desc">Use a pure-black dark theme</string>
|
<string name="set_black_mode_desc">Use a pure-black dark theme</string>
|
||||||
|
|
||||||
<string name="set_display">Display</string>
|
<string name="set_display">Display</string>
|
||||||
<string name="set_lib_tabs">Library tabs</string>
|
<string name="set_lib_tabs">Library tabs</string>
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
app:defaultValue="false"
|
app:defaultValue="false"
|
||||||
app:iconSpaceReserved="false"
|
app:iconSpaceReserved="false"
|
||||||
app:key="KEY_BLACK_THEME"
|
app:key="KEY_BLACK_THEME"
|
||||||
app:summary="@string/setting_black_mode_desc"
|
app:summary="@string/set_black_mode_desc"
|
||||||
app:title="@string/set_black_mode" />
|
app:title="@string/set_black_mode" />
|
||||||
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
|
@ -16,8 +16,7 @@ This is probably caused by one of two reasons:
|
||||||
|
|
||||||
#### I have a large library and Auxio takes really long to load it!
|
#### I have a large library and Auxio takes really long to load it!
|
||||||
|
|
||||||
This is expected since reading media takes awhile, especially with libraries containing 10k songs or more.
|
This is expected since reading from the audio database takes awhile, especially with libraries containing 10k songs or more.
|
||||||
I hope to mitigate this in the future by allowing one to customize the music loader to optimize for speed instead of accuracy.
|
|
||||||
|
|
||||||
#### Why ExoPlayer?
|
#### Why ExoPlayer?
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue