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
|
- Artists and album artists are now both given UI entires
|
||||||
- Upgraded music ID management:
|
- Upgraded music ID management:
|
||||||
- Use MD5 for default UUIDS
|
- 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)
|
- Added toggle to load non-music (Such as podcasts)
|
||||||
|
- Music loader now caches parsed metadata for faster load times
|
||||||
|
|
||||||
#### What's Improved
|
#### What's Improved
|
||||||
- Sorting now takes accented characters into account
|
- Sorting now takes accented characters into account
|
||||||
|
|
|
@ -3,6 +3,7 @@ plugins {
|
||||||
id "kotlin-android"
|
id "kotlin-android"
|
||||||
id "androidx.navigation.safeargs.kotlin"
|
id "androidx.navigation.safeargs.kotlin"
|
||||||
id "com.diffplug.spotless"
|
id "com.diffplug.spotless"
|
||||||
|
id "kotlin-kapt"
|
||||||
id "kotlin-parcelize"
|
id "kotlin-parcelize"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,6 +96,12 @@ dependencies {
|
||||||
// Preferences
|
// Preferences
|
||||||
implementation "androidx.preference:preference-ktx:1.2.0"
|
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 ---
|
// --- THIRD PARTY ---
|
||||||
|
|
||||||
// Exoplayer
|
// Exoplayer
|
||||||
|
|
|
@ -358,7 +358,7 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
||||||
musicBrainzId = raw.albumMusicBrainzId?.toUuidOrNull(),
|
musicBrainzId = raw.albumMusicBrainzId?.toUuidOrNull(),
|
||||||
name = requireNotNull(raw.albumName) { "Invalid raw: No album name" },
|
name = requireNotNull(raw.albumName) { "Invalid raw: No album name" },
|
||||||
sortName = raw.albumSortName,
|
sortName = raw.albumSortName,
|
||||||
releaseType = raw.albumReleaseType.parseReleaseType(settings),
|
releaseType = raw.albumReleaseTypes.parseReleaseType(settings),
|
||||||
rawArtists = rawAlbumArtists.ifEmpty { rawArtists }.ifEmpty { listOf(Artist.Raw(null, null)) }
|
rawArtists = rawAlbumArtists.ifEmpty { rawArtists }.ifEmpty { listOf(Artist.Raw(null, null)) }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -385,17 +385,17 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
||||||
class Raw
|
class Raw
|
||||||
constructor(
|
constructor(
|
||||||
var mediaStoreId: Long? = null,
|
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 dateAdded: Long? = null,
|
||||||
var dateModified: Long? = null,
|
var dateModified: Long? = null,
|
||||||
|
var fileName: String? = null,
|
||||||
|
var directory: Directory? = null,
|
||||||
|
var size: Long? = null,
|
||||||
var durationMs: 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 track: Int? = null,
|
||||||
var disc: Int? = null,
|
var disc: Int? = null,
|
||||||
var date: Date? = null,
|
var date: Date? = null,
|
||||||
|
@ -403,7 +403,7 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
||||||
var albumMusicBrainzId: String? = null,
|
var albumMusicBrainzId: String? = null,
|
||||||
var albumName: String? = null,
|
var albumName: String? = null,
|
||||||
var albumSortName: String? = null,
|
var albumSortName: String? = null,
|
||||||
var albumReleaseType: List<String> = listOf(),
|
var albumReleaseTypes: List<String> = listOf(),
|
||||||
var artistMusicBrainzIds: List<String> = listOf(),
|
var artistMusicBrainzIds: List<String> = listOf(),
|
||||||
var artistNames: List<String> = listOf(),
|
var artistNames: List<String> = listOf(),
|
||||||
var artistSortNames: 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.
|
* music loading process.
|
||||||
* @author OxygenCobalt
|
* @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 cursor: Cursor? = null
|
||||||
|
|
||||||
private var idIndex = -1
|
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.
|
// Populate the minimum required fields to maybe obtain a cache entry.
|
||||||
raw.mediaStoreId = cursor.getLong(idIndex)
|
populateFileData(cursor, raw)
|
||||||
raw.dateAdded = cursor.getLong(dateAddedIndex)
|
|
||||||
raw.dateModified = cursor.getLong(dateAddedIndex)
|
|
||||||
|
|
||||||
if (cacheDatabase.maybePopulateCachedRaw(raw)) {
|
if (cacheDatabase.populateFromCache(raw)) {
|
||||||
// We found a valid cache entry, no need to extract metadata.
|
// We found a valid cache entry, no need to extract metadata.
|
||||||
logD("Found cached raw: ${raw.name}")
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
buildRaw(cursor, raw)
|
populateMetadata(cursor, raw)
|
||||||
|
|
||||||
// We had to freshly make this raw, return false
|
// We had to freshly make this raw, return false
|
||||||
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
|
* The projection to use when querying media. Add version-specific columns here in an
|
||||||
* implementation.
|
* implementation.
|
||||||
*/
|
*/
|
||||||
open val projection: Array<String>
|
protected open val projection: Array<String>
|
||||||
get() =
|
get() =
|
||||||
arrayOf(
|
arrayOf(
|
||||||
// These columns are guaranteed to work on all versions of android
|
// 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
|
AUDIO_COLUMN_ALBUM_ARTIST
|
||||||
)
|
)
|
||||||
|
|
||||||
abstract val dirSelector: String
|
protected abstract val dirSelector: String
|
||||||
abstract fun addDirToSelectorArgs(dir: Directory, args: MutableList<String>): Boolean
|
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
|
* Populate the "file data" of the cursor, or data that is required to access a cache entry
|
||||||
* obtain an upstream [Song.Raw] first, and then populate it with version-specific fields
|
* or makes no sense to cache. This includes database IDs, modification dates,
|
||||||
* outlined in [projection].
|
|
||||||
*/
|
*/
|
||||||
protected open fun buildRaw(cursor: Cursor, raw: Song.Raw) {
|
protected open fun populateFileData(cursor: Cursor, raw: Song.Raw) {
|
||||||
raw.name = cursor.getString(titleIndex)
|
raw.mediaStoreId = cursor.getLong(idIndex)
|
||||||
|
raw.dateAdded = cursor.getLong(dateAddedIndex)
|
||||||
raw.extensionMimeType = cursor.getString(mimeTypeIndex)
|
raw.dateModified = cursor.getLong(dateAddedIndex)
|
||||||
raw.size = cursor.getLong(sizeIndex)
|
|
||||||
|
|
||||||
// Try to use the DISPLAY_NAME field to obtain a (probably sane) file name
|
// Try to use the DISPLAY_NAME field to obtain a (probably sane) file name
|
||||||
// from the android system.
|
// from the android system.
|
||||||
raw.fileName = cursor.getStringOrNull(displayNameIndex)
|
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.durationMs = cursor.getLong(durationIndex)
|
||||||
|
|
||||||
raw.date = cursor.getIntOrNull(yearIndex)?.toDate()
|
raw.date = cursor.getIntOrNull(yearIndex)?.toDate()
|
||||||
|
|
||||||
// A non-existent album name should theoretically be the name of the folder it contained
|
// 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
|
// file is not actually in the root internal storage directory. We can't do anything to
|
||||||
// fix this, really.
|
// fix this, really.
|
||||||
raw.albumName = cursor.getString(albumIndex)
|
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
|
// 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
|
// 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.
|
* API 21 onwards to API 29.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class Api21MediaStoreExtractor(context: Context, cacheDatabase: CacheDatabase) :
|
class Api21MediaStoreExtractor(context: Context, cacheDatabase: CacheExtractor) :
|
||||||
MediaStoreExtractor(context, cacheDatabase) {
|
MediaStoreExtractor(context, cacheDatabase) {
|
||||||
private var trackIndex = -1
|
private var trackIndex = -1
|
||||||
private var dataIndex = -1
|
private var dataIndex = -1
|
||||||
|
@ -401,8 +407,8 @@ class Api21MediaStoreExtractor(context: Context, cacheDatabase: CacheDatabase) :
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun buildRaw(cursor: Cursor, raw: Song.Raw) {
|
override fun populateFileData(cursor: Cursor, raw: Song.Raw) {
|
||||||
super.buildRaw(cursor, raw)
|
super.populateFileData(cursor, raw)
|
||||||
|
|
||||||
// DATA is equivalent to the absolute path of the file.
|
// DATA is equivalent to the absolute path of the file.
|
||||||
val data = cursor.getString(dataIndex)
|
val data = cursor.getString(dataIndex)
|
||||||
|
@ -426,6 +432,10 @@ class Api21MediaStoreExtractor(context: Context, cacheDatabase: CacheDatabase) :
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun populateMetadata(cursor: Cursor, raw: Song.Raw) {
|
||||||
|
super.populateMetadata(cursor, raw)
|
||||||
|
|
||||||
val rawTrack = cursor.getIntOrNull(trackIndex)
|
val rawTrack = cursor.getIntOrNull(trackIndex)
|
||||||
if (rawTrack != null) {
|
if (rawTrack != null) {
|
||||||
|
@ -441,7 +451,7 @@ class Api21MediaStoreExtractor(context: Context, cacheDatabase: CacheDatabase) :
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
open class BaseApi29MediaStoreExtractor(context: Context, cacheDatabase: CacheDatabase) :
|
open class BaseApi29MediaStoreExtractor(context: Context, cacheDatabase: CacheExtractor) :
|
||||||
MediaStoreExtractor(context, cacheDatabase) {
|
MediaStoreExtractor(context, cacheDatabase) {
|
||||||
private var volumeIndex = -1
|
private var volumeIndex = -1
|
||||||
private var relativePathIndex = -1
|
private var relativePathIndex = -1
|
||||||
|
@ -476,8 +486,8 @@ open class BaseApi29MediaStoreExtractor(context: Context, cacheDatabase: CacheDa
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun buildRaw(cursor: Cursor, raw: Song.Raw) {
|
override fun populateFileData(cursor: Cursor, raw: Song.Raw) {
|
||||||
super.buildRaw(cursor, raw)
|
super.populateFileData(cursor, raw)
|
||||||
|
|
||||||
val volumeName = cursor.getString(volumeIndex)
|
val volumeName = cursor.getString(volumeIndex)
|
||||||
val relativePath = cursor.getString(relativePathIndex)
|
val relativePath = cursor.getString(relativePathIndex)
|
||||||
|
@ -497,7 +507,7 @@ open class BaseApi29MediaStoreExtractor(context: Context, cacheDatabase: CacheDa
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
open class Api29MediaStoreExtractor(context: Context, cacheDatabase: CacheDatabase) :
|
open class Api29MediaStoreExtractor(context: Context, cacheDatabase: CacheExtractor) :
|
||||||
BaseApi29MediaStoreExtractor(context, cacheDatabase) {
|
BaseApi29MediaStoreExtractor(context, cacheDatabase) {
|
||||||
private var trackIndex = -1
|
private var trackIndex = -1
|
||||||
|
|
||||||
|
@ -510,8 +520,8 @@ open class Api29MediaStoreExtractor(context: Context, cacheDatabase: CacheDataba
|
||||||
override val projection: Array<String>
|
override val projection: Array<String>
|
||||||
get() = super.projection + arrayOf(MediaStore.Audio.AudioColumns.TRACK)
|
get() = super.projection + arrayOf(MediaStore.Audio.AudioColumns.TRACK)
|
||||||
|
|
||||||
override fun buildRaw(cursor: Cursor, raw: Song.Raw) {
|
override fun populateMetadata(cursor: Cursor, raw: Song.Raw) {
|
||||||
super.buildRaw(cursor, raw)
|
super.populateMetadata(cursor, raw)
|
||||||
|
|
||||||
// This backend is volume-aware, but does not support the modern track fields.
|
// This backend is volume-aware, but does not support the modern track fields.
|
||||||
// Use the old field instead.
|
// Use the old field instead.
|
||||||
|
@ -529,7 +539,7 @@ open class Api29MediaStoreExtractor(context: Context, cacheDatabase: CacheDataba
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
@RequiresApi(Build.VERSION_CODES.R)
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
class Api30MediaStoreExtractor(context: Context, cacheDatabase: CacheDatabase) :
|
class Api30MediaStoreExtractor(context: Context, cacheDatabase: CacheExtractor) :
|
||||||
BaseApi29MediaStoreExtractor(context, cacheDatabase) {
|
BaseApi29MediaStoreExtractor(context, cacheDatabase) {
|
||||||
private var trackIndex: Int = -1
|
private var trackIndex: Int = -1
|
||||||
private var discIndex: Int = -1
|
private var discIndex: Int = -1
|
||||||
|
@ -549,8 +559,8 @@ class Api30MediaStoreExtractor(context: Context, cacheDatabase: CacheDatabase) :
|
||||||
MediaStore.Audio.AudioColumns.DISC_NUMBER
|
MediaStore.Audio.AudioColumns.DISC_NUMBER
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun buildRaw(cursor: Cursor, raw: Song.Raw) {
|
override fun populateMetadata(cursor: Cursor, raw: Song.Raw) {
|
||||||
super.buildRaw(cursor, raw)
|
super.populateMetadata(cursor, raw)
|
||||||
|
|
||||||
// Both CD_TRACK_NUMBER and DISC_NUMBER tend to be formatted as they are in
|
// 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
|
// 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["TALB"]?.let { raw.albumName = it[0] }
|
||||||
tags["TSOA"]?.let { raw.albumSortName = it[0] }
|
tags["TSOA"]?.let { raw.albumSortName = it[0] }
|
||||||
(tags["TXXX:MusicBrainz Album Type"] ?: tags["GRP1"])?.let {
|
(tags["TXXX:MusicBrainz Album Type"] ?: tags["GRP1"])?.let {
|
||||||
raw.albumReleaseType = it
|
raw.albumReleaseTypes = it
|
||||||
}
|
}
|
||||||
|
|
||||||
// Artist
|
// Artist
|
||||||
tags["TXXX:MusicBrainz Artist Id"]?.let { raw.artistMusicBrainzIds = it }
|
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 }
|
tags["TSOP"]?.let { raw.artistSortNames = it }
|
||||||
|
|
||||||
// Album artist
|
// Album artist
|
||||||
|
@ -299,17 +299,17 @@ class Task(context: Context, private val raw: Song.Raw) {
|
||||||
tags["MUSICBRAINZ_ALBUMID"]?.let { raw.albumMusicBrainzId = it[0] }
|
tags["MUSICBRAINZ_ALBUMID"]?.let { raw.albumMusicBrainzId = it[0] }
|
||||||
tags["ALBUM"]?.let { raw.albumName = it[0] }
|
tags["ALBUM"]?.let { raw.albumName = it[0] }
|
||||||
tags["ALBUMSORT"]?.let { raw.albumSortName = it[0] }
|
tags["ALBUMSORT"]?.let { raw.albumSortName = it[0] }
|
||||||
tags["RELEASETYPE"]?.let { raw.albumReleaseType = it }
|
tags["RELEASETYPE"]?.let { raw.albumReleaseTypes = it }
|
||||||
|
|
||||||
// Artist
|
// Artist
|
||||||
tags["MUSICBRAINZ_ARTISTID"]?.let { raw.artistMusicBrainzIds = it }
|
tags["MUSICBRAINZ_ARTISTID"]?.let { raw.artistMusicBrainzIds = it }
|
||||||
(tags["ARTISTS"] ?: tags["ARTIST"])?.let { raw.artistNames = it }
|
tags["ARTIST"]?.let { raw.artistNames = it }
|
||||||
(tags["ARTISTS_SORT"] ?: tags["ARTISTSORT"])?.let { raw.artistSortNames = it }
|
tags["ARTISTSORT"]?.let { raw.artistSortNames = it }
|
||||||
|
|
||||||
// Album artist
|
// Album artist
|
||||||
tags["MUSICBRAINZ_ALBUMARTISTID"]?.let { raw.albumArtistMusicBrainzIds = it }
|
tags["MUSICBRAINZ_ALBUMARTISTID"]?.let { raw.albumArtistMusicBrainzIds = it }
|
||||||
(tags["ALBUMARTISTS"] ?: tags["ALBUMARTIST"])?.let { raw.albumArtistNames = it }
|
tags["ALBUMARTIST"]?.let { raw.albumArtistNames = it }
|
||||||
(tags["ALBUMARTISTS_SORT"] ?: tags["ALBUMARTISTSORT"])?.let { raw.albumArtistSortNames = it }
|
tags["ALBUMARTISTSORT"]?.let { raw.albumArtistSortNames = it }
|
||||||
|
|
||||||
// Genre
|
// Genre
|
||||||
tags["GENRE"]?.let { raw.genreNames = it }
|
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.Api21MediaStoreExtractor
|
||||||
import org.oxycblt.auxio.music.extractor.Api29MediaStoreExtractor
|
import org.oxycblt.auxio.music.extractor.Api29MediaStoreExtractor
|
||||||
import org.oxycblt.auxio.music.extractor.Api30MediaStoreExtractor
|
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.music.extractor.MetadataExtractor
|
||||||
import org.oxycblt.auxio.settings.Settings
|
import org.oxycblt.auxio.settings.Settings
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
@ -196,14 +196,12 @@ class Indexer {
|
||||||
* Run the proper music loading process.
|
* Run the proper music loading process.
|
||||||
*/
|
*/
|
||||||
private suspend fun indexImpl(context: Context): MusicStore.Library? {
|
private suspend fun indexImpl(context: Context): MusicStore.Library? {
|
||||||
emitIndexing(Indexing.Indeterminate)
|
|
||||||
|
|
||||||
// Create the chain of extractors. Each extractor builds on the previous and
|
// Create the chain of extractors. Each extractor builds on the previous and
|
||||||
// enables version-specific features in order to create the best possible music
|
// enables version-specific features in order to create the best possible music
|
||||||
// experience. This is technically dependency injection. Except it doesn't increase
|
// experience. This is technically dependency injection. Except it doesn't increase
|
||||||
// your compile times by 3x. Isn't that nice.
|
// your compile times by 3x. Isn't that nice.
|
||||||
|
|
||||||
val cacheDatabase = CacheDatabase()
|
val cacheDatabase = CacheExtractor(context)
|
||||||
|
|
||||||
val mediaStoreExtractor =
|
val mediaStoreExtractor =
|
||||||
when {
|
when {
|
||||||
|
@ -259,6 +257,7 @@ class Indexer {
|
||||||
logD("Starting indexing process")
|
logD("Starting indexing process")
|
||||||
|
|
||||||
val start = System.currentTimeMillis()
|
val start = System.currentTimeMillis()
|
||||||
|
emitIndexing(Indexing.Indeterminate)
|
||||||
|
|
||||||
// Initialize the extractor chain. This also nets us the projected total
|
// Initialize the extractor chain. This also nets us the projected total
|
||||||
// that we can show when loading.
|
// that we can show when loading.
|
||||||
|
@ -279,6 +278,8 @@ class Indexer {
|
||||||
emitIndexing(Indexing.Songs(songs.size, total))
|
emitIndexing(Indexing.Songs(songs.size, total))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
emitIndexing(Indexing.Indeterminate)
|
||||||
|
|
||||||
metadataExtractor.finalize(rawSongs)
|
metadataExtractor.finalize(rawSongs)
|
||||||
|
|
||||||
logD("Successfully built ${songs.size} songs in ${System.currentTimeMillis() - start}ms")
|
logD("Successfully built ${songs.size} songs in ${System.currentTimeMillis() - start}ms")
|
||||||
|
|
|
@ -282,11 +282,11 @@ class PlaybackStateDatabase private constructor(context: Context) :
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
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 DB_VERSION = 8
|
||||||
|
|
||||||
const val TABLE_STATE = "playback_state_table"
|
const val TABLE_STATE = "playback_state"
|
||||||
const val TABLE_QUEUE = "queue_table"
|
const val TABLE_QUEUE = "queue"
|
||||||
|
|
||||||
@Volatile private var INSTANCE: PlaybackStateDatabase? = null
|
@Volatile private var INSTANCE: PlaybackStateDatabase? = null
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue