music: add caching
Add caching of already-parsed tag data. This greatly reduces loading times when the music library has not changed. This completes the music loader in it's entirety now. Resolves #207.
This commit is contained in:
parent
393bdf3110
commit
3e73cd8080
9 changed files with 443 additions and 97 deletions
|
@ -10,8 +10,9 @@
|
|||
- Artists and album artists are now both given UI entires
|
||||
- Upgraded music ID management:
|
||||
- Use MD5 for default UUIDS
|
||||
- Added support for MusicBrainz IDs (MBIDs)
|
||||
- Added support for MusicBrainz IDs (MBIDs
|
||||
- Added toggle to load non-music (Such as podcasts)
|
||||
- Music loader now caches parsed metadata for faster load times
|
||||
|
||||
#### What's Improved
|
||||
- Sorting now takes accented characters into account
|
||||
|
|
|
@ -3,6 +3,7 @@ plugins {
|
|||
id "kotlin-android"
|
||||
id "androidx.navigation.safeargs.kotlin"
|
||||
id "com.diffplug.spotless"
|
||||
id "kotlin-kapt"
|
||||
id "kotlin-parcelize"
|
||||
}
|
||||
|
||||
|
@ -95,6 +96,12 @@ dependencies {
|
|||
// Preferences
|
||||
implementation "androidx.preference:preference-ktx:1.2.0"
|
||||
|
||||
// Room
|
||||
def room_version = "2.4.3"
|
||||
implementation "androidx.room:room-runtime:$room_version"
|
||||
kapt "androidx.room:room-compiler:$room_version"
|
||||
implementation "androidx.room:room-ktx:$room_version"
|
||||
|
||||
// --- THIRD PARTY ---
|
||||
|
||||
// Exoplayer
|
||||
|
|
|
@ -358,7 +358,7 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
|||
musicBrainzId = raw.albumMusicBrainzId?.toUuidOrNull(),
|
||||
name = requireNotNull(raw.albumName) { "Invalid raw: No album name" },
|
||||
sortName = raw.albumSortName,
|
||||
releaseType = raw.albumReleaseType.parseReleaseType(settings),
|
||||
releaseType = raw.albumReleaseTypes.parseReleaseType(settings),
|
||||
rawArtists = rawAlbumArtists.ifEmpty { rawArtists }.ifEmpty { listOf(Artist.Raw(null, null)) }
|
||||
)
|
||||
|
||||
|
@ -385,17 +385,17 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
|||
class Raw
|
||||
constructor(
|
||||
var mediaStoreId: Long? = null,
|
||||
var musicBrainzId: String? = null,
|
||||
var name: String? = null,
|
||||
var fileName: String? = null,
|
||||
var sortName: String? = null,
|
||||
var directory: Directory? = null,
|
||||
var extensionMimeType: String? = null,
|
||||
var formatMimeType: String? = null,
|
||||
var size: Long? = null,
|
||||
var dateAdded: Long? = null,
|
||||
var dateModified: Long? = null,
|
||||
var fileName: String? = null,
|
||||
var directory: Directory? = null,
|
||||
var size: Long? = null,
|
||||
var durationMs: Long? = null,
|
||||
var extensionMimeType: String? = null,
|
||||
var formatMimeType: String? = null,
|
||||
var musicBrainzId: String? = null,
|
||||
var name: String? = null,
|
||||
var sortName: String? = null,
|
||||
var track: Int? = null,
|
||||
var disc: Int? = null,
|
||||
var date: Date? = null,
|
||||
|
@ -403,7 +403,7 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
|||
var albumMusicBrainzId: String? = null,
|
||||
var albumName: String? = null,
|
||||
var albumSortName: String? = null,
|
||||
var albumReleaseType: List<String> = listOf(),
|
||||
var albumReleaseTypes: List<String> = listOf(),
|
||||
var artistMusicBrainzIds: List<String> = listOf(),
|
||||
var artistNames: List<String> = listOf(),
|
||||
var artistSortNames: List<String> = listOf(),
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2022 Auxio Project
|
||||
*
|
||||
* 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.extractor
|
||||
|
||||
import org.oxycblt.auxio.music.Song
|
||||
|
||||
/** TODO: Stub class, not implemented yet */
|
||||
class CacheDatabase {
|
||||
fun init() {
|
||||
}
|
||||
|
||||
// FIXME: Make the raw datatype use raw values, with most parsing being done in the song
|
||||
// constructor to ensure cache coherency
|
||||
|
||||
/**
|
||||
* Write a list of newly-indexed raw songs to the database.
|
||||
*/
|
||||
fun finalize(rawSongs: List<Song.Raw>) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Maybe copy a cached raw song into this instance, assuming that it has not changed
|
||||
* since it was last saved.
|
||||
*/
|
||||
fun maybePopulateCachedRaw(raw: Song.Raw) = false
|
||||
}
|
|
@ -0,0 +1,368 @@
|
|||
/*
|
||||
* Copyright (c) 2022 Auxio Project
|
||||
*
|
||||
* 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.extractor
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import androidx.core.database.getIntOrNull
|
||||
import androidx.core.database.getStringOrNull
|
||||
import androidx.core.database.sqlite.transaction
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.queryAll
|
||||
import org.oxycblt.auxio.util.requireBackgroundThread
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* The extractor that caches music metadata for faster use later. The cache is only responsible for
|
||||
* storing "intrinsic" data, as in information derived from the file format and not
|
||||
* information from the media database or file system. The exceptions are the database ID and
|
||||
* modification times for files, as these are required for the cache to function well.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class CacheExtractor(private val context: Context) {
|
||||
private var cacheMap: Map<Long, Song.Raw>? = null
|
||||
private var shouldWriteCache = false
|
||||
|
||||
fun init() {
|
||||
cacheMap = CacheDatabase.getInstance(context).read()
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a list of newly-indexed raw songs to the database.
|
||||
*/
|
||||
fun finalize(rawSongs: List<Song.Raw>) {
|
||||
cacheMap = null
|
||||
|
||||
if (shouldWriteCache) {
|
||||
// If the entire library could not be loaded from the cache, we need to re-write it
|
||||
// with the new library.
|
||||
logD("Cache was invalidated during loading, rewriting")
|
||||
CacheDatabase.getInstance(context).write(rawSongs)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maybe copy a cached raw song into this instance, assuming that it has not changed
|
||||
* since it was last saved.
|
||||
*/
|
||||
fun populateFromCache(rawSong: Song.Raw): Boolean {
|
||||
val map = requireNotNull(cacheMap) { "CacheExtractor was not properly initialized" }
|
||||
|
||||
val cachedRawSong = map[rawSong.mediaStoreId]
|
||||
if (cachedRawSong != null && cachedRawSong.dateAdded == rawSong.dateAdded && cachedRawSong.dateModified == rawSong.dateModified) {
|
||||
rawSong.musicBrainzId = cachedRawSong.musicBrainzId
|
||||
rawSong.name = cachedRawSong.name
|
||||
rawSong.sortName = cachedRawSong.sortName
|
||||
|
||||
rawSong.size = cachedRawSong.size
|
||||
rawSong.durationMs = cachedRawSong.durationMs
|
||||
rawSong.formatMimeType = cachedRawSong.formatMimeType
|
||||
|
||||
rawSong.track = cachedRawSong.track
|
||||
rawSong.disc = cachedRawSong.disc
|
||||
rawSong.date = cachedRawSong.date
|
||||
|
||||
rawSong.albumMusicBrainzId = cachedRawSong.albumMusicBrainzId
|
||||
rawSong.albumName = cachedRawSong.albumName
|
||||
rawSong.albumSortName = cachedRawSong.albumSortName
|
||||
rawSong.albumReleaseTypes = cachedRawSong.albumReleaseTypes
|
||||
|
||||
rawSong.artistMusicBrainzIds = cachedRawSong.artistMusicBrainzIds
|
||||
rawSong.artistNames = cachedRawSong.artistNames
|
||||
rawSong.artistSortNames = cachedRawSong.artistSortNames
|
||||
|
||||
rawSong.albumArtistMusicBrainzIds = cachedRawSong.albumArtistMusicBrainzIds
|
||||
rawSong.albumArtistNames = cachedRawSong.albumArtistNames
|
||||
rawSong.albumArtistSortNames = cachedRawSong.albumArtistSortNames
|
||||
|
||||
rawSong.genreNames = cachedRawSong.genreNames
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
shouldWriteCache = true
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private class CacheDatabase(context: Context) : SQLiteOpenHelper(context, File(context.cacheDir, DB_NAME).absolutePath, null, DB_VERSION) {
|
||||
override fun onCreate(db: SQLiteDatabase) {
|
||||
val command = StringBuilder()
|
||||
.append("CREATE TABLE IF NOT EXISTS $TABLE_RAW_SONGS(")
|
||||
.append("${Columns.MEDIA_STORE_ID} LONG PRIMARY KEY,")
|
||||
.append("${Columns.DATE_ADDED} LONG NOT NULL,")
|
||||
.append("${Columns.DATE_MODIFIED} LONG NOT NULL,")
|
||||
.append("${Columns.SIZE} LONG NOT NULL,")
|
||||
.append("${Columns.DURATION} LONG NOT NULL,")
|
||||
.append("${Columns.FORMAT_MIME_TYPE} STRING,")
|
||||
.append("${Columns.MUSIC_BRAINZ_ID} STRING,")
|
||||
.append("${Columns.NAME} STRING NOT NULL,")
|
||||
.append("${Columns.SORT_NAME} STRING,")
|
||||
.append("${Columns.TRACK} INT,")
|
||||
.append("${Columns.DISC} INT,")
|
||||
.append("${Columns.DATE} STRING,")
|
||||
.append("${Columns.ALBUM_MUSIC_BRAINZ_ID} STRING,")
|
||||
.append("${Columns.ALBUM_NAME} STRING NOT NULL,")
|
||||
.append("${Columns.ALBUM_SORT_NAME} STRING,")
|
||||
.append("${Columns.ALBUM_RELEASE_TYPES} STRING,")
|
||||
.append("${Columns.ARTIST_MUSIC_BRAINZ_IDS} STRING,")
|
||||
.append("${Columns.ARTIST_NAMES} STRING,")
|
||||
.append("${Columns.ARTIST_SORT_NAMES} STRING,")
|
||||
.append("${Columns.ALBUM_ARTIST_MUSIC_BRAINZ_IDS} STRING,")
|
||||
.append("${Columns.ALBUM_ARTIST_NAMES} STRING,")
|
||||
.append("${Columns.ALBUM_ARTIST_SORT_NAMES} STRING,")
|
||||
.append("${Columns.GENRE_NAMES} STRING)")
|
||||
|
||||
db.execSQL(command.toString())
|
||||
}
|
||||
|
||||
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) = nuke(db)
|
||||
|
||||
override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) = nuke(db)
|
||||
|
||||
private fun nuke(db: SQLiteDatabase) {
|
||||
logD("Nuking database")
|
||||
db.apply {
|
||||
execSQL("DROP TABLE IF EXISTS $TABLE_RAW_SONGS")
|
||||
onCreate(this)
|
||||
}
|
||||
}
|
||||
|
||||
fun read(): Map<Long, Song.Raw> {
|
||||
requireBackgroundThread()
|
||||
|
||||
val map = mutableMapOf<Long, Song.Raw>()
|
||||
|
||||
readableDatabase.queryAll(TABLE_RAW_SONGS) { cursor ->
|
||||
if (cursor.count == 0) return@queryAll
|
||||
|
||||
val idIndex = cursor.getColumnIndexOrThrow(Columns.MEDIA_STORE_ID)
|
||||
val dateAddedIndex = cursor.getColumnIndexOrThrow(Columns.DATE_ADDED)
|
||||
val dateModifiedIndex = cursor.getColumnIndexOrThrow(Columns.DATE_MODIFIED)
|
||||
|
||||
val sizeIndex = cursor.getColumnIndexOrThrow(Columns.SIZE)
|
||||
val durationIndex = cursor.getColumnIndexOrThrow(Columns.DURATION)
|
||||
val formatMimeTypeIndex = cursor.getColumnIndexOrThrow(Columns.FORMAT_MIME_TYPE)
|
||||
|
||||
val musicBrainzIdIndex = cursor.getColumnIndexOrThrow(Columns.MUSIC_BRAINZ_ID)
|
||||
val nameIndex = cursor.getColumnIndexOrThrow(Columns.NAME)
|
||||
val sortNameIndex = cursor.getColumnIndexOrThrow(Columns.SORT_NAME)
|
||||
|
||||
val trackIndex = cursor.getColumnIndexOrThrow(Columns.TRACK)
|
||||
val discIndex = cursor.getColumnIndexOrThrow(Columns.DISC)
|
||||
val dateIndex = cursor.getColumnIndexOrThrow(Columns.DATE)
|
||||
|
||||
val albumMusicBrainzIdIndex = cursor.getColumnIndexOrThrow(Columns.ALBUM_MUSIC_BRAINZ_ID)
|
||||
val albumNameIndex = cursor.getColumnIndexOrThrow(Columns.ALBUM_NAME)
|
||||
val albumSortNameIndex = cursor.getColumnIndexOrThrow(Columns.ALBUM_SORT_NAME)
|
||||
val albumReleaseTypesIndex = cursor.getColumnIndexOrThrow(Columns.ALBUM_RELEASE_TYPES)
|
||||
|
||||
val artistMusicBrainzIdsIndex = cursor.getColumnIndexOrThrow(Columns.ARTIST_MUSIC_BRAINZ_IDS)
|
||||
val artistNamesIndex = cursor.getColumnIndexOrThrow(Columns.ARTIST_NAMES)
|
||||
val artistSortNamesIndex = cursor.getColumnIndexOrThrow(Columns.ARTIST_SORT_NAMES)
|
||||
|
||||
val albumArtistMusicBrainzIdsIndex = cursor.getColumnIndexOrThrow(Columns.ALBUM_ARTIST_MUSIC_BRAINZ_IDS)
|
||||
val albumArtistNamesIndex = cursor.getColumnIndexOrThrow(Columns.ALBUM_ARTIST_NAMES)
|
||||
val albumArtistSortNamesIndex = cursor.getColumnIndexOrThrow(Columns.ALBUM_ARTIST_SORT_NAMES)
|
||||
|
||||
val genresIndex = cursor.getColumnIndexOrThrow(Columns.GENRE_NAMES)
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
val raw = Song.Raw()
|
||||
val id = cursor.getLong(idIndex)
|
||||
|
||||
raw.mediaStoreId = id
|
||||
raw.dateAdded = cursor.getLong(dateAddedIndex)
|
||||
raw.dateModified = cursor.getLong(dateModifiedIndex)
|
||||
|
||||
raw.size = cursor.getLong(sizeIndex)
|
||||
raw.durationMs = cursor.getLong(durationIndex)
|
||||
raw.formatMimeType = cursor.getStringOrNull(formatMimeTypeIndex)
|
||||
|
||||
raw.musicBrainzId = cursor.getStringOrNull(musicBrainzIdIndex)
|
||||
raw.name = cursor.getString(nameIndex)
|
||||
raw.sortName = cursor.getStringOrNull(sortNameIndex)
|
||||
|
||||
raw.track = cursor.getIntOrNull(trackIndex)
|
||||
raw.disc = cursor.getIntOrNull(discIndex)
|
||||
raw.date = cursor.getStringOrNull(dateIndex)?.parseTimestamp()
|
||||
|
||||
raw.albumMusicBrainzId = cursor.getStringOrNull(albumMusicBrainzIdIndex)
|
||||
raw.albumName = cursor.getString(albumNameIndex)
|
||||
raw.albumSortName = cursor.getStringOrNull(albumSortNameIndex)
|
||||
cursor.getStringOrNull(albumReleaseTypesIndex)?.parseMultiValue()
|
||||
?.let { raw.albumReleaseTypes = it }
|
||||
|
||||
cursor.getStringOrNull(artistMusicBrainzIdsIndex)?.let {
|
||||
raw.artistMusicBrainzIds = it.parseMultiValue()
|
||||
}
|
||||
cursor.getStringOrNull(artistNamesIndex)
|
||||
?.let { raw.artistNames = it.parseMultiValue() }
|
||||
cursor.getStringOrNull(artistSortNamesIndex)
|
||||
?.let { raw.artistSortNames = it.parseMultiValue() }
|
||||
|
||||
cursor.getStringOrNull(albumArtistMusicBrainzIdsIndex)
|
||||
?.let { raw.albumArtistMusicBrainzIds = it.parseMultiValue() }
|
||||
cursor.getStringOrNull(albumArtistNamesIndex)
|
||||
?.let { raw.albumArtistNames = it.parseMultiValue() }
|
||||
cursor.getStringOrNull(albumArtistSortNamesIndex)
|
||||
?.let { raw.albumArtistSortNames = it.parseMultiValue() }
|
||||
|
||||
cursor.getStringOrNull(genresIndex)
|
||||
?.let { raw.genreNames = it.parseMultiValue() }
|
||||
|
||||
map[id] = raw
|
||||
}
|
||||
}
|
||||
|
||||
return map
|
||||
}
|
||||
|
||||
fun write(rawSongs: List<Song.Raw>) {
|
||||
var position = 0
|
||||
val database = writableDatabase
|
||||
database.transaction { delete(TABLE_RAW_SONGS, null, null) }
|
||||
|
||||
logD("Cleared raw songs database")
|
||||
|
||||
while (position < rawSongs.size) {
|
||||
var i = position
|
||||
|
||||
database.transaction {
|
||||
while (i < rawSongs.size) {
|
||||
val rawSong = rawSongs[i]
|
||||
i++
|
||||
|
||||
val itemData =
|
||||
ContentValues(22).apply {
|
||||
put(Columns.MEDIA_STORE_ID, rawSong.mediaStoreId)
|
||||
put(Columns.DATE_ADDED, rawSong.dateAdded)
|
||||
put(Columns.DATE_MODIFIED, rawSong.dateModified)
|
||||
|
||||
put(Columns.SIZE, rawSong.size)
|
||||
put(Columns.DURATION, rawSong.durationMs)
|
||||
put(Columns.FORMAT_MIME_TYPE, rawSong.formatMimeType)
|
||||
|
||||
put(Columns.MUSIC_BRAINZ_ID, rawSong.name)
|
||||
put(Columns.NAME, rawSong.name)
|
||||
put(Columns.SORT_NAME, rawSong.sortName)
|
||||
|
||||
put(Columns.TRACK, rawSong.track)
|
||||
put(Columns.DISC, rawSong.disc)
|
||||
put(Columns.DATE, rawSong.date?.toString())
|
||||
|
||||
put(Columns.ALBUM_MUSIC_BRAINZ_ID, rawSong.albumMusicBrainzId)
|
||||
put(Columns.ALBUM_NAME, rawSong.albumName)
|
||||
put(Columns.ALBUM_SORT_NAME, rawSong.albumSortName)
|
||||
put(Columns.ALBUM_RELEASE_TYPES, rawSong.albumReleaseTypes.toMultiValue())
|
||||
|
||||
put(Columns.ARTIST_MUSIC_BRAINZ_IDS, rawSong.artistMusicBrainzIds.toMultiValue())
|
||||
put(Columns.ARTIST_NAMES, rawSong.artistNames.toMultiValue())
|
||||
put(Columns.ARTIST_SORT_NAMES, rawSong.artistSortNames.toMultiValue())
|
||||
|
||||
put(Columns.ALBUM_ARTIST_MUSIC_BRAINZ_IDS, rawSong.albumArtistMusicBrainzIds.toMultiValue())
|
||||
put(Columns.ALBUM_ARTIST_NAMES, rawSong.albumArtistNames.toMultiValue())
|
||||
put(Columns.ALBUM_ARTIST_SORT_NAMES, rawSong.albumArtistSortNames.toMultiValue())
|
||||
|
||||
put(Columns.GENRE_NAMES, rawSong.genreNames.toMultiValue())
|
||||
}
|
||||
|
||||
insert(TABLE_RAW_SONGS, null, itemData)
|
||||
}
|
||||
}
|
||||
|
||||
// Update the position at the end, if an insert failed at any point, then
|
||||
// the next iteration should skip it.
|
||||
position = i
|
||||
|
||||
logD("Wrote batch of raw songs. Position is now at $position")
|
||||
}
|
||||
}
|
||||
|
||||
// SQLite does not natively support multiple values, so we have to serialize multi-value
|
||||
// tags with separators. Not ideal, but nothing we can do.
|
||||
|
||||
private fun List<String>.toMultiValue() =
|
||||
if (isNotEmpty()) {
|
||||
joinToString(";") { it.replace(";", "\\;") }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
private fun String.parseMultiValue() = splitEscaped { it == ';' }
|
||||
|
||||
private object Columns {
|
||||
const val MEDIA_STORE_ID = "msid"
|
||||
const val DATE_ADDED = "date_added"
|
||||
const val DATE_MODIFIED = "date_modified"
|
||||
|
||||
const val SIZE = "size"
|
||||
const val DURATION = "duration"
|
||||
const val FORMAT_MIME_TYPE = "fmt_mime"
|
||||
|
||||
const val MUSIC_BRAINZ_ID = "mbid"
|
||||
const val NAME = "name"
|
||||
const val SORT_NAME = "sort_name"
|
||||
|
||||
const val TRACK = "track"
|
||||
const val DISC = "disc"
|
||||
const val DATE = "date"
|
||||
|
||||
const val ALBUM_MUSIC_BRAINZ_ID = "album_mbid"
|
||||
const val ALBUM_NAME = "album"
|
||||
const val ALBUM_SORT_NAME = "album_sort"
|
||||
const val ALBUM_RELEASE_TYPES = "album_types"
|
||||
|
||||
const val ARTIST_MUSIC_BRAINZ_IDS = "artists_mbid"
|
||||
const val ARTIST_NAMES = "artists"
|
||||
const val ARTIST_SORT_NAMES = "artists_sort"
|
||||
|
||||
const val ALBUM_ARTIST_MUSIC_BRAINZ_IDS = "album_artists_mbid"
|
||||
const val ALBUM_ARTIST_NAMES = "album_artists"
|
||||
const val ALBUM_ARTIST_SORT_NAMES = "album_artists_sort"
|
||||
|
||||
const val GENRE_NAMES = "genres"
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val DB_NAME = "auxio_music_cache.db"
|
||||
const val DB_VERSION = 1
|
||||
|
||||
const val TABLE_RAW_SONGS = "raw_songs"
|
||||
|
||||
@Volatile private var INSTANCE: CacheDatabase? = null
|
||||
|
||||
/** Get/Instantiate the single instance of [CacheDatabase]. */
|
||||
fun getInstance(context: Context): CacheDatabase {
|
||||
val currentInstance = INSTANCE
|
||||
|
||||
if (currentInstance != null) {
|
||||
return currentInstance
|
||||
}
|
||||
|
||||
synchronized(this) {
|
||||
val newInstance = CacheDatabase(context.applicationContext)
|
||||
INSTANCE = newInstance
|
||||
return newInstance
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -100,7 +100,7 @@ import java.io.File
|
|||
* music loading process.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
abstract class MediaStoreExtractor(private val context: Context, private val cacheDatabase: CacheDatabase) {
|
||||
abstract class MediaStoreExtractor(private val context: Context, private val cacheDatabase: CacheExtractor) {
|
||||
private var cursor: Cursor? = null
|
||||
|
||||
private var idIndex = -1
|
||||
|
@ -259,17 +259,14 @@ abstract class MediaStoreExtractor(private val context: Context, private val cac
|
|||
}
|
||||
|
||||
// Populate the minimum required fields to maybe obtain a cache entry.
|
||||
raw.mediaStoreId = cursor.getLong(idIndex)
|
||||
raw.dateAdded = cursor.getLong(dateAddedIndex)
|
||||
raw.dateModified = cursor.getLong(dateAddedIndex)
|
||||
populateFileData(cursor, raw)
|
||||
|
||||
if (cacheDatabase.maybePopulateCachedRaw(raw)) {
|
||||
if (cacheDatabase.populateFromCache(raw)) {
|
||||
// We found a valid cache entry, no need to extract metadata.
|
||||
logD("Found cached raw: ${raw.name}")
|
||||
return true
|
||||
}
|
||||
|
||||
buildRaw(cursor, raw)
|
||||
populateMetadata(cursor, raw)
|
||||
|
||||
// We had to freshly make this raw, return false
|
||||
return false
|
||||
|
@ -279,7 +276,7 @@ abstract class MediaStoreExtractor(private val context: Context, private val cac
|
|||
* The projection to use when querying media. Add version-specific columns here in an
|
||||
* implementation.
|
||||
*/
|
||||
open val projection: Array<String>
|
||||
protected open val projection: Array<String>
|
||||
get() =
|
||||
arrayOf(
|
||||
// These columns are guaranteed to work on all versions of android
|
||||
|
@ -298,25 +295,35 @@ abstract class MediaStoreExtractor(private val context: Context, private val cac
|
|||
AUDIO_COLUMN_ALBUM_ARTIST
|
||||
)
|
||||
|
||||
abstract val dirSelector: String
|
||||
abstract fun addDirToSelectorArgs(dir: Directory, args: MutableList<String>): Boolean
|
||||
protected abstract val dirSelector: String
|
||||
protected abstract fun addDirToSelectorArgs(dir: Directory, args: MutableList<String>): Boolean
|
||||
|
||||
/**
|
||||
* Build an [Song.Raw] based on the current cursor values. Each implementation should try to
|
||||
* obtain an upstream [Song.Raw] first, and then populate it with version-specific fields
|
||||
* outlined in [projection].
|
||||
* Populate the "file data" of the cursor, or data that is required to access a cache entry
|
||||
* or makes no sense to cache. This includes database IDs, modification dates,
|
||||
*/
|
||||
protected open fun buildRaw(cursor: Cursor, raw: Song.Raw) {
|
||||
raw.name = cursor.getString(titleIndex)
|
||||
|
||||
raw.extensionMimeType = cursor.getString(mimeTypeIndex)
|
||||
raw.size = cursor.getLong(sizeIndex)
|
||||
protected open fun populateFileData(cursor: Cursor, raw: Song.Raw) {
|
||||
raw.mediaStoreId = cursor.getLong(idIndex)
|
||||
raw.dateAdded = cursor.getLong(dateAddedIndex)
|
||||
raw.dateModified = cursor.getLong(dateAddedIndex)
|
||||
|
||||
// Try to use the DISPLAY_NAME field to obtain a (probably sane) file name
|
||||
// from the android system.
|
||||
raw.fileName = cursor.getStringOrNull(displayNameIndex)
|
||||
raw.extensionMimeType = cursor.getString(mimeTypeIndex)
|
||||
|
||||
raw.albumMediaStoreId = cursor.getLong(albumIdIndex)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract cursor metadata into [raw].
|
||||
*/
|
||||
protected open fun populateMetadata(cursor: Cursor, raw: Song.Raw) {
|
||||
raw.name = cursor.getString(titleIndex)
|
||||
|
||||
raw.size = cursor.getLong(sizeIndex)
|
||||
raw.durationMs = cursor.getLong(durationIndex)
|
||||
|
||||
raw.date = cursor.getIntOrNull(yearIndex)?.toDate()
|
||||
|
||||
// A non-existent album name should theoretically be the name of the folder it contained
|
||||
|
@ -324,7 +331,6 @@ abstract class MediaStoreExtractor(private val context: Context, private val cac
|
|||
// file is not actually in the root internal storage directory. We can't do anything to
|
||||
// fix this, really.
|
||||
raw.albumName = cursor.getString(albumIndex)
|
||||
raw.albumMediaStoreId = cursor.getLong(albumIdIndex)
|
||||
|
||||
// Android does not make a non-existent artist tag null, it instead fills it in
|
||||
// as <unknown>, which makes absolutely no sense given how other fields default
|
||||
|
@ -375,7 +381,7 @@ abstract class MediaStoreExtractor(private val context: Context, private val cac
|
|||
* API 21 onwards to API 29.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class Api21MediaStoreExtractor(context: Context, cacheDatabase: CacheDatabase) :
|
||||
class Api21MediaStoreExtractor(context: Context, cacheDatabase: CacheExtractor) :
|
||||
MediaStoreExtractor(context, cacheDatabase) {
|
||||
private var trackIndex = -1
|
||||
private var dataIndex = -1
|
||||
|
@ -401,8 +407,8 @@ class Api21MediaStoreExtractor(context: Context, cacheDatabase: CacheDatabase) :
|
|||
return true
|
||||
}
|
||||
|
||||
override fun buildRaw(cursor: Cursor, raw: Song.Raw) {
|
||||
super.buildRaw(cursor, raw)
|
||||
override fun populateFileData(cursor: Cursor, raw: Song.Raw) {
|
||||
super.populateFileData(cursor, raw)
|
||||
|
||||
// DATA is equivalent to the absolute path of the file.
|
||||
val data = cursor.getString(dataIndex)
|
||||
|
@ -426,6 +432,10 @@ class Api21MediaStoreExtractor(context: Context, cacheDatabase: CacheDatabase) :
|
|||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun populateMetadata(cursor: Cursor, raw: Song.Raw) {
|
||||
super.populateMetadata(cursor, raw)
|
||||
|
||||
val rawTrack = cursor.getIntOrNull(trackIndex)
|
||||
if (rawTrack != null) {
|
||||
|
@ -441,7 +451,7 @@ class Api21MediaStoreExtractor(context: Context, cacheDatabase: CacheDatabase) :
|
|||
* @author OxygenCobalt
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
open class BaseApi29MediaStoreExtractor(context: Context, cacheDatabase: CacheDatabase) :
|
||||
open class BaseApi29MediaStoreExtractor(context: Context, cacheDatabase: CacheExtractor) :
|
||||
MediaStoreExtractor(context, cacheDatabase) {
|
||||
private var volumeIndex = -1
|
||||
private var relativePathIndex = -1
|
||||
|
@ -476,8 +486,8 @@ open class BaseApi29MediaStoreExtractor(context: Context, cacheDatabase: CacheDa
|
|||
return true
|
||||
}
|
||||
|
||||
override fun buildRaw(cursor: Cursor, raw: Song.Raw) {
|
||||
super.buildRaw(cursor, raw)
|
||||
override fun populateFileData(cursor: Cursor, raw: Song.Raw) {
|
||||
super.populateFileData(cursor, raw)
|
||||
|
||||
val volumeName = cursor.getString(volumeIndex)
|
||||
val relativePath = cursor.getString(relativePathIndex)
|
||||
|
@ -497,7 +507,7 @@ open class BaseApi29MediaStoreExtractor(context: Context, cacheDatabase: CacheDa
|
|||
* @author OxygenCobalt
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
open class Api29MediaStoreExtractor(context: Context, cacheDatabase: CacheDatabase) :
|
||||
open class Api29MediaStoreExtractor(context: Context, cacheDatabase: CacheExtractor) :
|
||||
BaseApi29MediaStoreExtractor(context, cacheDatabase) {
|
||||
private var trackIndex = -1
|
||||
|
||||
|
@ -510,8 +520,8 @@ open class Api29MediaStoreExtractor(context: Context, cacheDatabase: CacheDataba
|
|||
override val projection: Array<String>
|
||||
get() = super.projection + arrayOf(MediaStore.Audio.AudioColumns.TRACK)
|
||||
|
||||
override fun buildRaw(cursor: Cursor, raw: Song.Raw) {
|
||||
super.buildRaw(cursor, raw)
|
||||
override fun populateMetadata(cursor: Cursor, raw: Song.Raw) {
|
||||
super.populateMetadata(cursor, raw)
|
||||
|
||||
// This backend is volume-aware, but does not support the modern track fields.
|
||||
// Use the old field instead.
|
||||
|
@ -529,7 +539,7 @@ open class Api29MediaStoreExtractor(context: Context, cacheDatabase: CacheDataba
|
|||
* @author OxygenCobalt
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
class Api30MediaStoreExtractor(context: Context, cacheDatabase: CacheDatabase) :
|
||||
class Api30MediaStoreExtractor(context: Context, cacheDatabase: CacheExtractor) :
|
||||
BaseApi29MediaStoreExtractor(context, cacheDatabase) {
|
||||
private var trackIndex: Int = -1
|
||||
private var discIndex: Int = -1
|
||||
|
@ -549,8 +559,8 @@ class Api30MediaStoreExtractor(context: Context, cacheDatabase: CacheDatabase) :
|
|||
MediaStore.Audio.AudioColumns.DISC_NUMBER
|
||||
)
|
||||
|
||||
override fun buildRaw(cursor: Cursor, raw: Song.Raw) {
|
||||
super.buildRaw(cursor, raw)
|
||||
override fun populateMetadata(cursor: Cursor, raw: Song.Raw) {
|
||||
super.populateMetadata(cursor, raw)
|
||||
|
||||
// Both CD_TRACK_NUMBER and DISC_NUMBER tend to be formatted as they are in
|
||||
// the tag itself, which is to say that it is formatted as NN/TT tracks, where
|
||||
|
|
|
@ -227,12 +227,12 @@ class Task(context: Context, private val raw: Song.Raw) {
|
|||
tags["TALB"]?.let { raw.albumName = it[0] }
|
||||
tags["TSOA"]?.let { raw.albumSortName = it[0] }
|
||||
(tags["TXXX:MusicBrainz Album Type"] ?: tags["GRP1"])?.let {
|
||||
raw.albumReleaseType = it
|
||||
raw.albumReleaseTypes = it
|
||||
}
|
||||
|
||||
// Artist
|
||||
tags["TXXX:MusicBrainz Artist Id"]?.let { raw.artistMusicBrainzIds = it }
|
||||
(tags["TXXX:ARTISTS"] ?: tags["TPE1"])?.let { raw.artistNames = it }
|
||||
tags["TPE1"]?.let { raw.artistNames = it }
|
||||
tags["TSOP"]?.let { raw.artistSortNames = it }
|
||||
|
||||
// Album artist
|
||||
|
@ -299,17 +299,17 @@ class Task(context: Context, private val raw: Song.Raw) {
|
|||
tags["MUSICBRAINZ_ALBUMID"]?.let { raw.albumMusicBrainzId = it[0] }
|
||||
tags["ALBUM"]?.let { raw.albumName = it[0] }
|
||||
tags["ALBUMSORT"]?.let { raw.albumSortName = it[0] }
|
||||
tags["RELEASETYPE"]?.let { raw.albumReleaseType = it }
|
||||
tags["RELEASETYPE"]?.let { raw.albumReleaseTypes = it }
|
||||
|
||||
// Artist
|
||||
tags["MUSICBRAINZ_ARTISTID"]?.let { raw.artistMusicBrainzIds = it }
|
||||
(tags["ARTISTS"] ?: tags["ARTIST"])?.let { raw.artistNames = it }
|
||||
(tags["ARTISTS_SORT"] ?: tags["ARTISTSORT"])?.let { raw.artistSortNames = it }
|
||||
tags["ARTIST"]?.let { raw.artistNames = it }
|
||||
tags["ARTISTSORT"]?.let { raw.artistSortNames = it }
|
||||
|
||||
// Album artist
|
||||
tags["MUSICBRAINZ_ALBUMARTISTID"]?.let { raw.albumArtistMusicBrainzIds = it }
|
||||
(tags["ALBUMARTISTS"] ?: tags["ALBUMARTIST"])?.let { raw.albumArtistNames = it }
|
||||
(tags["ALBUMARTISTS_SORT"] ?: tags["ALBUMARTISTSORT"])?.let { raw.albumArtistSortNames = it }
|
||||
tags["ALBUMARTIST"]?.let { raw.albumArtistNames = it }
|
||||
tags["ALBUMARTISTSORT"]?.let { raw.albumArtistSortNames = it }
|
||||
|
||||
// Genre
|
||||
tags["GENRE"]?.let { raw.genreNames = it }
|
||||
|
|
|
@ -37,7 +37,7 @@ import org.oxycblt.auxio.music.Sort
|
|||
import org.oxycblt.auxio.music.extractor.Api21MediaStoreExtractor
|
||||
import org.oxycblt.auxio.music.extractor.Api29MediaStoreExtractor
|
||||
import org.oxycblt.auxio.music.extractor.Api30MediaStoreExtractor
|
||||
import org.oxycblt.auxio.music.extractor.CacheDatabase
|
||||
import org.oxycblt.auxio.music.extractor.CacheExtractor
|
||||
import org.oxycblt.auxio.music.extractor.MetadataExtractor
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
@ -196,14 +196,12 @@ class Indexer {
|
|||
* Run the proper music loading process.
|
||||
*/
|
||||
private suspend fun indexImpl(context: Context): MusicStore.Library? {
|
||||
emitIndexing(Indexing.Indeterminate)
|
||||
|
||||
// Create the chain of extractors. Each extractor builds on the previous and
|
||||
// enables version-specific features in order to create the best possible music
|
||||
// experience. This is technically dependency injection. Except it doesn't increase
|
||||
// your compile times by 3x. Isn't that nice.
|
||||
|
||||
val cacheDatabase = CacheDatabase()
|
||||
val cacheDatabase = CacheExtractor(context)
|
||||
|
||||
val mediaStoreExtractor =
|
||||
when {
|
||||
|
@ -259,6 +257,7 @@ class Indexer {
|
|||
logD("Starting indexing process")
|
||||
|
||||
val start = System.currentTimeMillis()
|
||||
emitIndexing(Indexing.Indeterminate)
|
||||
|
||||
// Initialize the extractor chain. This also nets us the projected total
|
||||
// that we can show when loading.
|
||||
|
@ -279,6 +278,8 @@ class Indexer {
|
|||
emitIndexing(Indexing.Songs(songs.size, total))
|
||||
}
|
||||
|
||||
emitIndexing(Indexing.Indeterminate)
|
||||
|
||||
metadataExtractor.finalize(rawSongs)
|
||||
|
||||
logD("Successfully built ${songs.size} songs in ${System.currentTimeMillis() - start}ms")
|
||||
|
|
|
@ -282,11 +282,11 @@ class PlaybackStateDatabase private constructor(context: Context) :
|
|||
}
|
||||
|
||||
companion object {
|
||||
const val DB_NAME = "auxio_state_database.db"
|
||||
const val DB_NAME = "auxio_playback_state.db"
|
||||
const val DB_VERSION = 8
|
||||
|
||||
const val TABLE_STATE = "playback_state_table"
|
||||
const val TABLE_QUEUE = "queue_table"
|
||||
const val TABLE_STATE = "playback_state"
|
||||
const val TABLE_QUEUE = "queue"
|
||||
|
||||
@Volatile private var INSTANCE: PlaybackStateDatabase? = null
|
||||
|
||||
|
|
Loading…
Reference in a new issue