diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b2566afb..e02157491 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/app/build.gradle b/app/build.gradle index 28ae802c2..f5cd92af5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 diff --git a/app/src/main/java/org/oxycblt/auxio/music/Music.kt b/app/src/main/java/org/oxycblt/auxio/music/Music.kt index 112341741..13e6998ae 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -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 = listOf(), + var albumReleaseTypes: List = listOf(), var artistMusicBrainzIds: List = listOf(), var artistNames: List = listOf(), var artistSortNames: List = listOf(), diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/CacheDatabase.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/CacheDatabase.kt deleted file mode 100644 index dd973a9e0..000000000 --- a/app/src/main/java/org/oxycblt/auxio/music/extractor/CacheDatabase.kt +++ /dev/null @@ -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 . - */ - -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) { - } - - /** - * 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 -} diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/CacheExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/CacheExtractor.kt new file mode 100644 index 000000000..e2d08c794 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/CacheExtractor.kt @@ -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 . + */ + +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? = 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) { + 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 { + requireBackgroundThread() + + val map = mutableMapOf() + + 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) { + 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.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 + } + } + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/MediaStoreExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/MediaStoreExtractor.kt index dbc66508b..56b3a0b88 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/extractor/MediaStoreExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/MediaStoreExtractor.kt @@ -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 + protected open val projection: Array 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): Boolean + protected abstract val dirSelector: String + protected abstract fun addDirToSelectorArgs(dir: Directory, args: MutableList): 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 , 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 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 diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/MetadataExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/MetadataExtractor.kt index 8cfdf4580..20bde63e6 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/extractor/MetadataExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/MetadataExtractor.kt @@ -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 } diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt b/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt index 722c7ab37..e64134331 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt @@ -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") diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateDatabase.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateDatabase.kt index c6597ed5a..ccfd1147f 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateDatabase.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateDatabase.kt @@ -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