diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f7e00163..1b6c701c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,12 @@ audio focus was lost - Fixed issue where the app would crash if a song menu in the genre UI was opened +#### What's Changed +- Ignore MediaStore tags is now on by default + #### Dev/Meta - Completed migration to reactive playback system +- Refactor music backends into a unified chain of extractors ## 2.6.3 diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt index 088acfa15..ae9d7ac98 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt @@ -83,6 +83,7 @@ abstract class DetailAdapter( return item is Header || item is SortHeader } + @Suppress("LeakingThis") protected val differ = AsyncListDiffer(this, diffCallback) override val currentList: List diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt index 331196ff3..8df39f973 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt @@ -39,9 +39,6 @@ import org.oxycblt.auxio.util.inflater */ class GenreDetailAdapter(private val listener: Listener) : DetailAdapter(listener, DIFFER) { - private var currentSong: Song? = null - private var isPlaying = false - override fun getItemViewType(position: Int) = when (differ.currentList[position]) { is Genre -> GenreDetailViewHolder.VIEW_TYPE diff --git a/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt b/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt index ed3783104..79c03ae25 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt @@ -57,7 +57,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr AppCompatImageView(context, attrs, defStyleAttr) { private val settings = Settings(context) - var cornerRadius = 0f + private var cornerRadius = 0f set(value) { field = value (background as? MaterialShapeDrawable)?.let { bg -> 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 cb6889a93..a2e6270e7 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -23,7 +23,6 @@ import android.content.Context import android.os.Parcelable import java.security.MessageDigest import java.util.UUID -import kotlin.experimental.and import kotlin.math.max import kotlin.math.min import kotlin.reflect.KClass @@ -143,7 +142,7 @@ sealed class MusicParent : Music() { * A song. * @author OxygenCobalt */ -class Song constructor(private val raw: Raw) : Music() { +class Song constructor(raw: Raw) : Music() { override val uid: UID override val rawName = requireNotNull(raw.name) { "Invalid raw: No title" } @@ -279,6 +278,7 @@ class Song constructor(private val raw: Raw) : Music() { var formatMimeType: String? = null, var size: Long? = null, var dateAdded: Long? = null, + var dateModified: Long? = null, var durationMs: Long? = null, var track: Int? = null, var disc: Int? = null, @@ -473,7 +473,7 @@ fun MessageDigest.update(date: Date?) { update(date.toString().toByteArray()) } -// Note: All methods regarding integer bytemucking must be little-endian +// Note: All methods regarding integer byte-mucking must be little-endian /** * Update the digest using the little-endian byte representation of a byte, or do not update if diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicUtil.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicUtil.kt index d67038c95..42fedbdf2 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicUtil.kt @@ -24,7 +24,6 @@ import android.database.Cursor import android.net.Uri import android.provider.MediaStore import android.text.format.DateUtils -import androidx.core.text.isDigitsOnly import org.oxycblt.auxio.R import org.oxycblt.auxio.util.logD import java.util.UUID diff --git a/app/src/main/java/org/oxycblt/auxio/music/StorageFramework.kt b/app/src/main/java/org/oxycblt/auxio/music/StorageFramework.kt index bd9d5b60c..6b6d4d6a7 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/StorageFramework.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/StorageFramework.kt @@ -18,11 +18,7 @@ package org.oxycblt.auxio.music import android.annotation.SuppressLint -import android.content.ContentResolver -import android.content.ContentUris import android.content.Context -import android.database.Cursor -import android.net.Uri import android.os.Build import android.os.Environment import android.os.storage.StorageManager diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/CacheLayer.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/CacheLayer.kt new file mode 100644 index 000000000..893fc14a1 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/CacheLayer.kt @@ -0,0 +1,18 @@ +package org.oxycblt.auxio.music.extractor + +import org.oxycblt.auxio.music.Song + +/** + * TODO: Stub class, not implemented yet + */ +class CacheLayer { + fun init() { + // STUB: Add cache database + } + + fun finalize(rawSongs: List) { + // STUB: Add cache database + } + + fun maybePopulateCachedRaw(raw: Song.Raw) = false +} \ No newline at end of file diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/MediaStoreBackend.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/MediaStoreLayer.kt similarity index 65% rename from app/src/main/java/org/oxycblt/auxio/music/system/MediaStoreBackend.kt rename to app/src/main/java/org/oxycblt/auxio/music/extractor/MediaStoreLayer.kt index 2a50024f1..cb666c8e0 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/MediaStoreBackend.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/MediaStoreLayer.kt @@ -1,21 +1,4 @@ -/* - * 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.system +package org.oxycblt.auxio.music.extractor import android.content.Context import android.database.Cursor @@ -26,12 +9,18 @@ import android.provider.MediaStore import androidx.annotation.RequiresApi import androidx.core.database.getIntOrNull import androidx.core.database.getStringOrNull -import java.io.File -import org.oxycblt.auxio.music.* +import org.oxycblt.auxio.music.Date +import org.oxycblt.auxio.music.Directory +import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.music.directoryCompat +import org.oxycblt.auxio.music.mediaStoreVolumeNameCompat +import org.oxycblt.auxio.music.queryCursor +import org.oxycblt.auxio.music.storageVolumesCompat import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.util.contentResolverSafe import org.oxycblt.auxio.util.getSystemServiceCompat import org.oxycblt.auxio.util.logD +import java.io.File /* * This file acts as the base for most the black magic required to get a remotely sensible music @@ -91,33 +80,41 @@ import org.oxycblt.auxio.util.logD * I wish I was born in the neolithic. */ -// TODO: Move duration util to MusicUtil - /** - * Represents a [Indexer.Backend] that loads music from the media database ([MediaStore]). This is - * not a fully-featured class by itself, and it's API-specific derivatives should be used instead. + * The layer that loads music from the MediaStore database. This is an intermediate step in + * the music loading process. * @author OxygenCobalt */ -abstract class MediaStoreBackend(private val context: Context) : Indexer.Backend { +abstract class MediaStoreLayer(private val context: Context, private val cacheLayer: CacheLayer) { + private var cursor: Cursor? = null + private var idIndex = -1 private var titleIndex = -1 private var displayNameIndex = -1 private var mimeTypeIndex = -1 private var sizeIndex = -1 private var dateAddedIndex = -1 + private var dateModifiedIndex = -1 private var durationIndex = -1 private var yearIndex = -1 private var albumIndex = -1 private var albumIdIndex = -1 private var artistIndex = -1 private var albumArtistIndex = -1 - + private val settings = Settings(context) - protected val volumes = mutableListOf() - override fun query(): Cursor { + private val _volumes = mutableListOf() + protected val volumes: List get() = _volumes + + /** + * Initialize this instance by making a query over the media database. + */ + open fun init(): Cursor { + cacheLayer.init() + val storageManager = context.getSystemServiceCompat(StorageManager::class) - volumes.addAll(storageManager.storageVolumesCompat) + _volumes.addAll(storageManager.storageVolumesCompat) val dirs = settings.getMusicDirs(storageManager) val args = mutableListOf() @@ -152,73 +149,70 @@ abstract class MediaStoreBackend(private val context: Context) : Indexer.Backend logD("Starting query [proj: ${projection.toList()}, selector: $selector, args: $args]") - return requireNotNull( + val cursor = requireNotNull( context.contentResolverSafe.queryCursor( MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selector, args.toTypedArray())) { "Content resolver failure: No Cursor returned" } + .also { cursor = it } + + idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns._ID) + titleIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TITLE) + displayNameIndex = + cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DISPLAY_NAME) + mimeTypeIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.MIME_TYPE) + sizeIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.SIZE) + dateAddedIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DATE_ADDED) + dateModifiedIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DATE_MODIFIED) + durationIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DURATION) + yearIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.YEAR) + albumIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ALBUM) + albumIdIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ALBUM_ID) + artistIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ARTIST) + albumArtistIndex = cursor.getColumnIndexOrThrow(AUDIO_COLUMN_ALBUM_ARTIST) + + return cursor } - override fun buildSongs( - cursor: Cursor, - emitIndexing: (Indexer.Indexing) -> Unit - ): List { - val rawSongs = mutableListOf() - while (cursor.moveToNext()) { - rawSongs.add(buildRawSong(context, cursor)) - if (cursor.position % 50 == 0) { - // Only check for a cancellation every 50 songs or so (~20ms). - // While this seems redundant, each call to emitIndexing checks for a - // cancellation of the co-routine this loading task is running on. - emitIndexing(Indexer.Indexing.Indeterminate) - } + /** + * Finalize this instance by closing the cursor and finalizing the cache. + */ + fun finalize(rawSongs: List) { + cursor?.close() + cursor = null + + cacheLayer.finalize(rawSongs) + } + + /** + * Populate a [raw] with whatever the next value in the cursor is. + * + * This returns true if the song could be restored from cache, false if metadata had to be + * re-extracted, and null if the cursor is exhausted. + */ + fun populateRaw(raw: Song.Raw): Boolean? { + val cursor = requireNotNull(cursor) { "MediaStoreLayer is not properly initialized" } + if (!cursor.moveToNext()) { + logD("Cursor is exhausted") + return null } - // The raw song is not actually complete at this point, as we cannot obtain a genre - // through a song query. Instead, we have to do the hack where we iterate through - // every genre and assign it's name to raw songs that match it's child IDs. - context.contentResolverSafe.useQuery( - MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI, - arrayOf(MediaStore.Audio.Genres._ID, MediaStore.Audio.Genres.NAME)) { genreCursor -> - val idIndex = genreCursor.getColumnIndexOrThrow(MediaStore.Audio.Genres._ID) - val nameIndex = genreCursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.NAME) + // 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) - while (genreCursor.moveToNext()) { - // Genre names could theoretically be anything, including null for some reason. - // Null values are junk and should be ignored, but since we cannot assume the - // format a genre was derived from, we have to treat them like they are ID3 - // genres, even when they might not be. - val id = genreCursor.getLong(idIndex) - val name = (genreCursor.getStringOrNull(nameIndex) ?: continue) - .parseId3GenreNames(settings) - - context.contentResolverSafe.useQuery( - MediaStore.Audio.Genres.Members.getContentUri(VOLUME_EXTERNAL, id), - arrayOf(MediaStore.Audio.Genres.Members._ID)) { cursor -> - val songIdIndex = - cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.Members._ID) - - while (cursor.moveToNext()) { - val songId = cursor.getLong(songIdIndex) - rawSongs - .find { it.mediaStoreId == songId } - ?.let { song -> song.genreNames = name } - - if (cursor.position % 50 == 0) { - // Only check for a cancellation every 50 songs or so (~20ms). - emitIndexing(Indexer.Indexing.Indeterminate) - } - } - } - } - - // Check for a cancellation every time we finish a genre too, in the case that - // the genre has <50 songs. - emitIndexing(Indexer.Indexing.Indeterminate) + if (cacheLayer.maybePopulateCachedRaw(raw)) { + // We found a valid cache entry, no need to build it. + logD("Found cached raw: ${raw.name}") + return true } - return rawSongs.map { Song(it) } + buildRaw(cursor, raw) + + // We had to freshly make this raw, return false + return false } /** @@ -235,6 +229,7 @@ abstract class MediaStoreBackend(private val context: Context) : Indexer.Backend MediaStore.Audio.AudioColumns.MIME_TYPE, MediaStore.Audio.AudioColumns.SIZE, MediaStore.Audio.AudioColumns.DATE_ADDED, + MediaStore.Audio.AudioColumns.DATE_MODIFIED, MediaStore.Audio.AudioColumns.DURATION, MediaStore.Audio.AudioColumns.YEAR, MediaStore.Audio.AudioColumns.ALBUM, @@ -250,32 +245,11 @@ abstract class MediaStoreBackend(private val context: Context) : Indexer.Backend * obtain an upstream [Song.Raw] first, and then populate it with version-specific fields * outlined in [projection]. */ - open fun buildRawSong(context: Context, cursor: Cursor): Song.Raw { - // Initialize our cursor indices if we haven't already. - if (idIndex == -1) { - idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns._ID) - titleIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TITLE) - displayNameIndex = - cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DISPLAY_NAME) - mimeTypeIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.MIME_TYPE) - sizeIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.SIZE) - dateAddedIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DATE_ADDED) - durationIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DURATION) - yearIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.YEAR) - albumIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ALBUM) - albumIdIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ALBUM_ID) - artistIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ARTIST) - albumArtistIndex = cursor.getColumnIndexOrThrow(AUDIO_COLUMN_ALBUM_ARTIST) - } - - val raw = Song.Raw() - - raw.mediaStoreId = cursor.getLong(idIndex) + protected open fun buildRaw(cursor: Cursor, raw: Song.Raw) { raw.name = cursor.getString(titleIndex) raw.extensionMimeType = cursor.getString(mimeTypeIndex) raw.size = cursor.getLong(sizeIndex) - raw.dateAdded = cursor.getLong(dateAddedIndex) // Try to use the DISPLAY_NAME field to obtain a (probably sane) file name // from the android system. @@ -294,11 +268,12 @@ abstract class MediaStoreBackend(private val context: Context) : Indexer.Backend // 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 // to null if they are not present. If this field is , null it so that - // it's easier to handle later. While we can't natively parse multi-value tags, - // from MediaStore itself, we can still parse by user-defined separators. + // it's easier to handle later. raw.artistNames = cursor.getString(artistIndex).run { if (this != MediaStore.UNKNOWN_STRING) { + // While we can't natively parse multi-value tags, + // from MediaStore itself, we can still parse by user-defined separators. maybeParseSeparators(settings) } else { null @@ -307,8 +282,6 @@ abstract class MediaStoreBackend(private val context: Context) : Indexer.Backend // The album artist field is nullable and never has placeholder values. raw.albumArtistNames = cursor.getStringOrNull(albumArtistIndex)?.maybeParseSeparators(settings) - - return raw } companion object { @@ -321,12 +294,6 @@ abstract class MediaStoreBackend(private val context: Context) : Indexer.Backend @Suppress("InlinedApi") private const val AUDIO_COLUMN_ALBUM_ARTIST = MediaStore.Audio.AudioColumns.ALBUM_ARTIST - /** - * External has existed since at least API 21, but no constant existed for it until API 29. - * This constant is safe to use. - */ - @Suppress("InlinedApi") private const val VOLUME_EXTERNAL = MediaStore.VOLUME_EXTERNAL - /** * The base selector that works across all versions of android. Does not exclude * directories. @@ -336,17 +303,26 @@ abstract class MediaStoreBackend(private val context: Context) : Indexer.Backend } } + // Note: The separation between version-specific backends may not be the cleanest. To preserve // speed, we only want to add redundancy on known issues, not with possible issues. /** - * A [MediaStoreBackend] that completes the music loading process in a way compatible from + * A [MediaStoreLayer] that completes the music loading process in a way compatible from * @author OxygenCobalt */ -class Api21MediaStoreBackend(context: Context) : MediaStoreBackend(context) { +class Api21MediaStoreLayer(context: Context, cacheLayer: CacheLayer) : + MediaStoreLayer(context, cacheLayer) { private var trackIndex = -1 private var dataIndex = -1 + override fun init(): Cursor { + val cursor = super.init() + trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK) + dataIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DATA) + return cursor + } + override val projection: Array get() = super.projection + @@ -361,15 +337,10 @@ class Api21MediaStoreBackend(context: Context) : MediaStoreBackend(context) { return true } - override fun buildRawSong(context: Context, cursor: Cursor): Song.Raw { - val raw = super.buildRawSong(context, cursor) - - // Initialize our indices if we have not already. - if (trackIndex == -1) { - trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK) - dataIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DATA) - } - + override fun buildRaw(cursor: Cursor, raw: Song.Raw) { + super.buildRaw(cursor, raw) + + // DATA is equivalent to the absolute path of the file. val data = cursor.getString(dataIndex) // On some OEM devices below API 29, DISPLAY_NAME may not be present. I assume @@ -397,21 +368,29 @@ class Api21MediaStoreBackend(context: Context) : MediaStoreBackend(context) { rawTrack.unpackTrackNo()?.let { raw.track = it } rawTrack.unpackDiscNo()?.let { raw.disc = it } } - - return raw } } /** - * A [MediaStoreBackend] that selects directories and builds paths using the modern volume fields + * A [MediaStoreLayer] that selects directories and builds paths using the modern volume fields * available from API 29 onwards. * @author OxygenCobalt */ @RequiresApi(Build.VERSION_CODES.Q) -open class BaseApi29MediaStoreBackend(context: Context) : MediaStoreBackend(context) { +open class BaseApi29MediaStoreLayer(context: Context, cacheLayer: CacheLayer) : MediaStoreLayer(context, cacheLayer) { private var volumeIndex = -1 private var relativePathIndex = -1 + override fun init(): Cursor { + val cursor = super.init() + + volumeIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.VOLUME_NAME) + relativePathIndex = + cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.RELATIVE_PATH) + + return cursor + } + override val projection: Array get() = super.projection + @@ -431,14 +410,8 @@ open class BaseApi29MediaStoreBackend(context: Context) : MediaStoreBackend(cont return true } - override fun buildRawSong(context: Context, cursor: Cursor): Song.Raw { - val raw = super.buildRawSong(context, cursor) - - if (volumeIndex == -1) { - volumeIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.VOLUME_NAME) - relativePathIndex = - cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.RELATIVE_PATH) - } + override fun buildRaw(cursor: Cursor, raw: Song.Raw) { + super.buildRaw(cursor, raw) val volumeName = cursor.getString(volumeIndex) val relativePath = cursor.getString(relativePathIndex) @@ -449,30 +422,30 @@ open class BaseApi29MediaStoreBackend(context: Context) : MediaStoreBackend(cont if (volume != null) { raw.directory = Directory.from(volume, relativePath) } - - return raw } } /** - * A [MediaStoreBackend] that completes the music loading process in a way compatible with at least + * A [MediaStoreLayer] that completes the music loading process in a way compatible with at least * API 29. * @author OxygenCobalt */ @RequiresApi(Build.VERSION_CODES.Q) -open class Api29MediaStoreBackend(context: Context) : BaseApi29MediaStoreBackend(context) { +open class Api29MediaStoreLayer(context: Context, cacheLayer: CacheLayer) : BaseApi29MediaStoreLayer(context, cacheLayer) { private var trackIndex = -1 + override fun init(): Cursor { + val cursor = super.init() + trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK) + return cursor + } + override val projection: Array get() = super.projection + arrayOf(MediaStore.Audio.AudioColumns.TRACK) - override fun buildRawSong(context: Context, cursor: Cursor): Song.Raw { - val raw = super.buildRawSong(context, cursor) - - if (trackIndex == -1) { - trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK) - } - + override fun buildRaw(cursor: Cursor, raw: Song.Raw) { + super.buildRaw(cursor, raw) + // This backend is volume-aware, but does not support the modern track fields. // Use the old field instead. val rawTrack = cursor.getIntOrNull(trackIndex) @@ -480,21 +453,26 @@ open class Api29MediaStoreBackend(context: Context) : BaseApi29MediaStoreBackend rawTrack.unpackTrackNo()?.let { raw.track = it } rawTrack.unpackDiscNo()?.let { raw.disc = it } } - - return raw } } /** - * A [MediaStoreBackend] that completes the music loading process in a way compatible with at least + * A [MediaStoreLayer] that completes the music loading process in a way compatible with at least * API 30. * @author OxygenCobalt */ @RequiresApi(Build.VERSION_CODES.R) -class Api30MediaStoreBackend(context: Context) : BaseApi29MediaStoreBackend(context) { +class Api30MediaStoreLayer(context: Context, cacheLayer: CacheLayer) : BaseApi29MediaStoreLayer(context, cacheLayer) { private var trackIndex: Int = -1 private var discIndex: Int = -1 + override fun init(): Cursor { + val cursor = super.init() + trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.CD_TRACK_NUMBER) + discIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DISC_NUMBER) + return cursor + } + override val projection: Array get() = super.projection + @@ -502,14 +480,8 @@ class Api30MediaStoreBackend(context: Context) : BaseApi29MediaStoreBackend(cont MediaStore.Audio.AudioColumns.CD_TRACK_NUMBER, MediaStore.Audio.AudioColumns.DISC_NUMBER) - override fun buildRawSong(context: Context, cursor: Cursor): Song.Raw { - val raw = super.buildRawSong(context, cursor) - - // Populate our indices if we have not already. - if (trackIndex == -1) { - trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.CD_TRACK_NUMBER) - discIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DISC_NUMBER) - } + override fun buildRaw(cursor: Cursor, raw: Song.Raw) { + super.buildRaw(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 @@ -517,7 +489,5 @@ class Api30MediaStoreBackend(context: Context) : BaseApi29MediaStoreBackend(cont // total, as we have no use for it. cursor.getStringOrNull(trackIndex)?.parsePositionNum()?.let { raw.track = it } cursor.getStringOrNull(discIndex)?.parsePositionNum()?.let { raw.disc = it } - - return raw } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/ExoPlayerBackend.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/MetadataLayer.kt similarity index 82% rename from app/src/main/java/org/oxycblt/auxio/music/system/ExoPlayerBackend.kt rename to app/src/main/java/org/oxycblt/auxio/music/extractor/MetadataLayer.kt index 237aa095d..58b4180e3 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/ExoPlayerBackend.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/MetadataLayer.kt @@ -1,39 +1,22 @@ -/* - * 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.system +package org.oxycblt.auxio.music.extractor import android.content.Context -import android.database.Cursor import androidx.core.text.isDigitsOnly import com.google.android.exoplayer2.MediaItem import com.google.android.exoplayer2.MetadataRetriever -import com.google.android.exoplayer2.metadata.Metadata import com.google.android.exoplayer2.metadata.id3.TextInformationFrame import com.google.android.exoplayer2.metadata.vorbis.VorbisComment -import org.oxycblt.auxio.music.Date import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.audioUri import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.util.logD +import com.google.android.exoplayer2.metadata.Metadata +import org.oxycblt.auxio.music.Date import org.oxycblt.auxio.util.logW + /** - * A [Indexer.Backend] that leverages ExoPlayer's metadata retrieval system to index metadata. + * The layer that leverages ExoPlayer's metadata retrieval system to index metadata. * * Normally, leveraging ExoPlayer's metadata system would be a terrible idea, as it is horrifically * slow. However, if we parallelize it, we can get similar throughput to other metadata extractors, @@ -45,29 +28,28 @@ import org.oxycblt.auxio.util.logW * * @author OxygenCobalt */ -class ExoPlayerBackend(private val context: Context, private val inner: MediaStoreBackend) : Indexer.Backend { +class MetadataLayer(private val context: Context, private val mediaStoreLayer: MediaStoreLayer) { private val settings = Settings(context) private val taskPool: Array = arrayOfNulls(TASK_CAPACITY) - // No need to implement our own query logic, as this backend is still reliant on - // MediaStore. - override fun query() = inner.query() + /** + * Initialize the sub-layers that this layer relies on. + */ + fun init() = mediaStoreLayer.init().count - override fun buildSongs( - cursor: Cursor, - emitIndexing: (Indexer.Indexing) -> Unit - ): List { - // Metadata retrieval with ExoPlayer is asynchronous, so a callback may at any point - // add a completed song to the list. To prevent a crash in that case, we use the - // concurrent counterpart to a typical mutable list. - val songs = mutableListOf() - val total = cursor.count + /** + * Finalize the sub-layers that this layer relies on. + */ + fun finalize(rawSongs: List) = mediaStoreLayer.finalize(rawSongs) - while (cursor.moveToNext()) { - // Note: This call to buildRawSong does not populate the genre field. This is - // because indexing genres is quite slow with MediaStore, and so keeping the - // field blank on unsupported ExoPlayer formats ends up being preferable. - val raw = inner.buildRawSong(context, cursor) + fun parse(emit: (Song.Raw) -> Unit) { + while (true) { + val raw = Song.Raw() + if (mediaStoreLayer.populateRaw(raw) ?: break) { + // No need to extract metadata that was successfully restored from the cache + emit(raw) + continue + } // Spin until there is an open slot we can insert a task in. Note that we do // not add callbacks to our new tasks, as Future callbacks run on a different @@ -78,10 +60,9 @@ class ExoPlayerBackend(private val context: Context, private val inner: MediaSto val task = taskPool[i] if (task != null) { - val song = task.get() - if (song != null) { - songs.add(song) - emitIndexing(Indexer.Indexing.Songs(songs.size, total)) + val finishedRaw = task.get() + if (finishedRaw != null) { + emit(finishedRaw) taskPool[i] = Task(context, settings, raw) break@spin } @@ -99,19 +80,17 @@ class ExoPlayerBackend(private val context: Context, private val inner: MediaSto val task = taskPool[i] if (task != null) { - val song = task.get() ?: continue@spin - songs.add(song) - emitIndexing(Indexer.Indexing.Songs(songs.size, total)) + val finishedRaw = task.get() ?: continue@spin + emit(finishedRaw) taskPool[i] = null } } break } - - return songs } + companion object { /** The amount of tasks this backend can run efficiently at once. */ private const val TASK_CAPACITY = 8 @@ -132,7 +111,7 @@ class Task(context: Context, private val settings: Settings, private val raw: So * Get the song that this task is trying to complete. If the task is still busy, this will * return null. Otherwise, it will return a song. */ - fun get(): Song? { + fun get(): Song.Raw? { if (!future.isDone) { return null } @@ -148,7 +127,7 @@ class Task(context: Context, private val settings: Settings, private val raw: So if (format == null) { logD("Nothing could be extracted for ${raw.name}") - return Song(raw) + return raw } // Populate the format mime type if we have one. @@ -161,7 +140,7 @@ class Task(context: Context, private val settings: Settings, private val raw: So logD("No metadata could be extracted for ${raw.name}") } - return Song(raw) + return raw } private fun completeRawSong(metadata: Metadata) { diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/ParsingUtil.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/ParsingUtil.kt similarity index 99% rename from app/src/main/java/org/oxycblt/auxio/music/system/ParsingUtil.kt rename to app/src/main/java/org/oxycblt/auxio/music/extractor/ParsingUtil.kt index 36fc44d42..9f80693ff 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/ParsingUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/ParsingUtil.kt @@ -1,4 +1,4 @@ -package org.oxycblt.auxio.music.system +package org.oxycblt.auxio.music.extractor import androidx.core.text.isDigitsOnly import org.oxycblt.auxio.music.Date 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 b801c25d6..f2f2381d6 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 @@ -20,7 +20,6 @@ package org.oxycblt.auxio.music.system import android.Manifest import android.content.Context import android.content.pm.PackageManager -import android.database.Cursor import android.os.Build import androidx.core.content.ContextCompat import kotlinx.coroutines.CancellationException @@ -28,13 +27,16 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.music.* -import org.oxycblt.auxio.settings.Settings +import org.oxycblt.auxio.music.extractor.Api21MediaStoreLayer +import org.oxycblt.auxio.music.extractor.Api29MediaStoreLayer +import org.oxycblt.auxio.music.extractor.Api30MediaStoreLayer +import org.oxycblt.auxio.music.extractor.CacheLayer +import org.oxycblt.auxio.music.extractor.MetadataLayer import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.util.TaskGuard import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logW -import org.oxycblt.auxio.util.requireBackgroundThread /** * Auxio's media indexer. @@ -43,13 +45,13 @@ import org.oxycblt.auxio.util.requireBackgroundThread * (and hacky garbage) in order to produce the best possible experience. It is split into three * distinct steps: * - * 1. Finding a [Backend] to use and then querying the media database with it. - * 2. Using the [Backend] and the media data to create songs + * 1. Creating the chain of layers to extract metadata with + * 2. Running the chain process * 3. Using the songs to build the library, which primarily involves linking up all data objects * with their corresponding parents/children. * * This class in particular handles 3 primarily. For the code that handles 1 and 2, see the - * [Backend] implementations. + * layer implementations. * * This class also fulfills the role of maintaining the current music loading state, which seems * like a job for [MusicStore] but in practice is only really leveraged by the components that @@ -194,27 +196,23 @@ class Indexer { private fun indexImpl(context: Context, handle: Long): MusicStore.Library? { emitIndexing(Indexing.Indeterminate, handle) - // Since we have different needs for each version, we determine a "Backend" to use - // when loading music and then leverage that to create the initial song list. - // This is technically dependency injection. Except it doesn't increase your compile - // times by 3x. Isn't that nice. + // Create the chain of layers. Each layer builds on the previous layer 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 mediaStoreBackend = + val cacheLayer = CacheLayer() + + val mediaStoreLayer = when { - Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> Api30MediaStoreBackend(context) - Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> Api29MediaStoreBackend(context) - else -> Api21MediaStoreBackend(context) + Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> Api30MediaStoreLayer(context, cacheLayer) + Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> Api29MediaStoreLayer(context, cacheLayer) + else -> Api21MediaStoreLayer(context, cacheLayer) } - val settings = Settings(context) - val backend = - if (settings.useQualityTags) { - ExoPlayerBackend(context, mediaStoreBackend) - } else { - mediaStoreBackend - } + val metadataLayer = MetadataLayer(context, mediaStoreLayer) - val songs = buildSongs(backend, handle) + val songs = buildSongs(metadataLayer, handle) if (songs.isEmpty()) { return null } @@ -236,32 +234,37 @@ class Indexer { } /** - * Does the initial query over the song database using [backend]. The songs returned by this + * Does the initial query over the song database using [metadataLayer]. The songs returned by this * function are **not** well-formed. The companion [buildAlbums], [buildArtists], and * [buildGenres] functions must be called with the returned list so that all songs are properly * linked up. */ - private fun buildSongs(backend: Backend, handle: Long): List { + private fun buildSongs(metadataLayer: MetadataLayer, handle: Long): List { val start = System.currentTimeMillis() - var songs = - backend.query().use { cursor -> - logD( - "Successfully queried media database " + - "in ${System.currentTimeMillis() - start}ms") + // Initialize the extractor chain. This also nets us the projected total + // that we can show when loading. + val total = metadataLayer.init() - backend.buildSongs(cursor) { emitIndexing(it, handle) } - } + // Note: We use a set here so we can eliminate effective duplicates of + // songs (by UID). + val songs = mutableSetOf() + val rawSongs = mutableListOf() - // Deduplicate songs to prevent (most) deformed music clones - songs = songs.distinctBy { it.uid }.toMutableList() + metadataLayer.parse { raw -> + songs.add(Song(raw)) + rawSongs.add(raw) + emitIndexing(Indexing.Songs(songs.size, total), handle) + } - // Ensure that sorting order is consistent so that grouping is also consistent. - Sort(Sort.Mode.ByName, true).songsInPlace(songs) + metadataLayer.finalize(rawSongs) + + val sorted = Sort(Sort.Mode.ByName, true).songs(songs) logD("Successfully built ${songs.size} songs in ${System.currentTimeMillis() - start}ms") - return songs + // Ensure that sorting order is consistent so that grouping is also consistent. + return sorted } /** @@ -420,18 +423,6 @@ class Indexer { fun onStartIndexing() } - /** Represents a backend that metadata can be extracted from. */ - interface Backend { - /** Query the media database for a basic cursor. */ - fun query(): Cursor - - /** Create a list of songs from the [Cursor] queried in [query]. */ - fun buildSongs( - cursor: Cursor, - emitIndexing: (Indexing) -> Unit - ): List - } - companion object { @Volatile private var INSTANCE: Indexer? = null diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerNotifications.kt b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerNotifications.kt index f2acf3c43..e348348b6 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerNotifications.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerNotifications.kt @@ -54,6 +54,7 @@ class IndexingNotification(private val context: Context) : } is Indexer.Indexing.Songs -> { // Only update the notification every 50 songs to prevent excessive updates. + // TODO: Use a timeout instead to handle rapid-fire updates w/o rate limiting if (indexing.current % 50 == 0) { logD("Updating state to $indexing") setContentText( diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt index c116e5df5..ce0248749 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt @@ -218,8 +218,7 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback { override fun onSettingChanged(key: String) { when (key) { getString(R.string.set_key_music_dirs), - getString(R.string.set_key_music_dirs_include), - getString(R.string.set_key_quality_tags) -> onStartIndexing() + getString(R.string.set_key_music_dirs_include)-> onStartIndexing() getString(R.string.set_key_observing) -> { if (!indexer.isIndexing) { updateIdleSession() diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt index abbc4d930..b382e5d6b 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt @@ -20,7 +20,6 @@ package org.oxycblt.auxio.playback import android.os.Bundle import android.view.LayoutInflater import androidx.fragment.app.activityViewModels -import kotlin.math.max import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentPlaybackBarBinding diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt index b96b69552..87184e45b 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt @@ -479,7 +479,6 @@ class PlaybackService : } companion object { - private const val POS_POLL_INTERVAL = 100L private const val REWIND_THRESHOLD = 3000L const val ACTION_INC_REPEAT_MODE = BuildConfig.APPLICATION_ID + ".action.LOOP" diff --git a/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt b/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt index 1961957f7..1749e56f4 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt @@ -189,10 +189,6 @@ class Settings(private val context: Context, private val callback: Callback? = n val pauseOnRepeat: Boolean get() = inner.getBoolean(context.getString(R.string.set_key_repeat_pause), false) - /** Whether to parse metadata directly with ExoPlayer. */ - val useQualityTags: Boolean - get() = inner.getBoolean(context.getString(R.string.set_key_quality_tags), false) - /** Whether to be actively watching for changes in the music library. */ val shouldBeObserving: Boolean get() = inner.getBoolean(context.getString(R.string.set_key_observing), false) diff --git a/app/src/main/java/org/oxycblt/auxio/ui/recycler/Data.kt b/app/src/main/java/org/oxycblt/auxio/ui/recycler/Data.kt index 7227b7eca..8408c487d 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/recycler/Data.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/recycler/Data.kt @@ -45,7 +45,7 @@ interface MenuItemListener : ItemClickListener { } /** - * Like [AsyncListDiffer], but synchronous. This may seem like it would be inefficient, but in + * Like AsyncListDiffer, but synchronous. This may seem like it would be inefficient, but in * practice Auxio's lists tend to be small enough to the point where this does not matter, and * situations that would be inefficient are ruled out with [replaceList]. */ diff --git a/app/src/main/java/org/oxycblt/auxio/util/PrimitiveUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/PrimitiveUtil.kt index 588e150ab..a8c165a16 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/PrimitiveUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/PrimitiveUtil.kt @@ -18,13 +18,11 @@ package org.oxycblt.auxio.util import android.os.Looper -import android.text.format.DateUtils import java.lang.reflect.Field import java.lang.reflect.Method import java.util.concurrent.CancellationException import kotlin.reflect.KClass import org.oxycblt.auxio.BuildConfig -import java.util.* /** Assert that we are on a background thread. */ fun requireBackgroundThread() { diff --git a/app/src/main/res/color/sel_remote_fab_ripple.xml b/app/src/main/res/color/sel_remote_fab_ripple.xml deleted file mode 100644 index 10337c895..000000000 --- a/app/src/main/res/color/sel_remote_fab_ripple.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 6cda30c88..b2751fb21 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -203,8 +203,6 @@ Přenosová rychlost Vzorkovací frekvence Hudební složky - Ignorovat štítky MediaStore - Zvýší kvalitu štítků, ale má za následek delší načítací čas (experimentální) Obnovit stav přehrávání Stav obnoven Obnovit dříve uložený stav přehrávání (pokud existuje) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 03574d3f7..fd7b2262e 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -196,8 +196,6 @@ Format Größe Bitrate - MediaStore-Tags ignorieren - Erhöht Tag-Qualität, benötigt aber längere Ladezeiten (Experimentell) Überwachen der Musikbibliothek Musik wird geladen Musikbibliothek wird auf Änderungen überwacht… diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 6a3d59076..9c14181c0 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -188,10 +188,8 @@ Reestablecer el estado de reproducción Reestablecer el estado de reproducción guardado previamente (si existe) Carpetas de música - Ignorar etiquetas MediaStore Gestionar de dónde se cargará la música La músicasolo se cargará de las carpetas que añadas. - Incrementa la calidad de las etiquetas, pero resulra en tiempos de carga mayores (Experimental) Formato desconocido Dinámico Disco %d diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 230c867c6..3cf0a7685 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -103,8 +103,6 @@ Glazba se neće učitati iz dodanih mapa. Uključi Glazba će se učitati samo iz dodanih mapa. - Zanemari MediaStore oznake - Poboljšava kvalitetu oznaka, no može produljiti vrijeme učitavanja (Eksperimentalno) Automatsko ponovno učitavanje Ponovo učitaj svoju zbirku glazbe čim se dogode promjene (eksperimentalno) Nijedan zvučni zapis nije pronađen diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index ef6722bd8..ef07b93bb 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -204,8 +204,6 @@ Stato ripristinato Data aggiunta Ricaricamento automatico - Ignora tags MediaStore - Migliora qualità dei tag, ma potrebbe richiedere tempi di carimento più lunghi (sperimentale) Ripristina stato riproduzione Ripristina lo stato di riproduzione precedentemente salvato (se disponibile) Impossibile ripristinare lo stato diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index a916e9e19..1a0cdf312 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -168,8 +168,6 @@ Ieškokite savo bibliotekoje… Ekvalaizeris Režimas - Ignoruoti „MediaStore“ žymas - Padidėja žymų kokybė, tačiau dėl to pailgėja krovimo laikas (Eksperimentinis) Automatinis krovimas Muzikos nerasta \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 818efadde..400cc06ee 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -170,8 +170,6 @@ Afspelen vanaf getoond item Afspeelstatus herstellen Herstel de eerder opgeslagen afspeelstatus (indien aanwezig) - MediaStore tags negeren - Verhoogt tag-kwaliteit, maar vereist langere laadtijden Geen staat kan hersteld worden Verwijder dit wachtrij liedje Verplaats dit wachtrij liedje diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 789f52b5f..53db8d397 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -193,9 +193,7 @@ Abas da biblioteca Gênero Reproduzir do artista - Aumenta a qualidade da tag, mas resulta em tempos de carregamento mais longos (Experimental) Restaurar o estado de reprodução salvo anteriormente (se houver) - Ignorar tags do MediaStore Ajuste com tags Estado liberado Estado restaurado diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 5f78a76e1..1d6b1a6d6 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -215,10 +215,8 @@ Перезагружать библиотеку при каждом изменении (экспериментально) -%.1f дБ Жанров загружено: %d - Игнорировать теги MediaStore Восстановить предыдущее состояние воспроизведения (если есть) Режим - Улучшает качество тегов, но приводит к долгому времени загрузки (экспериментально) Музыка будет загружена только из указанных папок. Невозможно восстановить состояние воспроизведения Нет номера трека diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 4bbe438a7..53297acfd 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -193,8 +193,6 @@ Önceden kaydedilmiş oynatma durumunu geri getirir (varsa) Yuvarlak mod Oynatma durumunu eski haline getir - MediaStore etiketlerini yoksay - Etiket kalitesini artırır, ancak daha uzun yükleme süreleri gerektirir Hiçbir durum geri getirelemedi Tekrarda duraklat Müzik yükleniyor diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index ac7698779..667ab9b3c 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -200,7 +200,6 @@ 没有可以恢复的状态 恢复播放状态 恢复此前保存的播放状态(如果有) - 提高标签质量,但会导致加载时间变长(实验性) 自动重载中 发生更改时自动重新加载您的曲库(实验性) 打开队列 @@ -209,7 +208,6 @@ 正在监测您的曲库以查找更改… 已清除状态 已恢复状态 - 忽略 MediaStore 标签 EP 专辑 EP 专辑 单曲 diff --git a/app/src/main/res/values/settings.xml b/app/src/main/res/values/settings.xml index 69fa968a1..7f518038c 100644 --- a/app/src/main/res/values/settings.xml +++ b/app/src/main/res/values/settings.xml @@ -34,7 +34,6 @@ auxio_include_dirs auxio_separators auxio_observing - auxio_quality_tags KEY_SEARCH_FILTER diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bd1cc44a8..fd43460ea 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -224,8 +224,6 @@ Include Music will only be loaded from the folders you add. - Ignore MediaStore tags - Increases tag quality, but results in longer loading times (Experimental) Automatic reloading Reload your music library whenever it changes (Experimental) diff --git a/app/src/main/res/values/styles_ui.xml b/app/src/main/res/values/styles_ui.xml index fe63f41cc..2f1ad1050 100644 --- a/app/src/main/res/values/styles_ui.xml +++ b/app/src/main/res/values/styles_ui.xml @@ -70,15 +70,6 @@ true - - diff --git a/app/src/main/res/xml/prefs_main.xml b/app/src/main/res/xml/prefs_main.xml index 214d186eb..7d4df744f 100644 --- a/app/src/main/res/xml/prefs_main.xml +++ b/app/src/main/res/xml/prefs_main.xml @@ -124,10 +124,6 @@ app:summary="@string/set_repeat_pause_desc" app:title="@string/set_repeat_pause" /> - - - - + + + + - -