From 55597e49760d2d9e2da33e520e0d436a380feee1 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Wed, 7 Jun 2023 09:22:20 -0600 Subject: [PATCH 01/26] ui: fix incorrectly aligned disc headers Fix an issue where disc headers would be at the top when no subtitle as available. I really thought that TextView.text would be null if I set it to null. Instead it becomes an empty string, breaking the visbility change. Fix it by just using the disc value. Resolves #472. --- .../org/oxycblt/auxio/detail/list/AlbumDetailListAdapter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/list/AlbumDetailListAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/list/AlbumDetailListAdapter.kt index 66fc29d7c..63419e1e5 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/list/AlbumDetailListAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/list/AlbumDetailListAdapter.kt @@ -115,7 +115,7 @@ private class DiscHeaderViewHolder(private val binding: ItemDiscHeaderBinding) : binding.discNumber.text = binding.context.getString(R.string.fmt_disc_no, disc.number) binding.discName.apply { text = disc.name - isGone = text == null + isGone = disc.name == null } } else { logD("Disc is null, defaulting to no disc") From 96874b332480e88a3909b54c0677b7afb1a5ea10 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Wed, 7 Jun 2023 09:42:12 -0600 Subject: [PATCH 02/26] music: do not transfer cache db Do not transfer the cache db between devices, drop it instead. This is at best useless (timestamps and ids will be invalid) and at worst actively detrimental (timestamps and ids are similar), so better to reindex than try to read from the cache. Resolves #467. --- CHANGELOG.md | 5 +++++ app/src/main/AndroidManifest.xml | 2 +- app/src/main/res/xml/backup_descriptor.xml | 5 ++++- app/src/main/res/xml/data_extraction_rules.xml | 9 +++++++-- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c2556181..f8281d98b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## dev + +#### What's Fixed +- Disc number is no longer mis-aligned when no subtitle is present + ## 3.1.1 #### What's New diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 81daa45e1..fd7b28a4a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -24,12 +24,12 @@ android:name=".Auxio" android:allowBackup="true" android:fullBackupContent="@xml/backup_descriptor" + android:dataExtractionRules="@xml/data_extraction_rules" android:icon="@mipmap/ic_launcher" android:label="@string/info_app_name" android:roundIcon="@mipmap/ic_launcher" android:supportsRtl="true" android:theme="@style/Theme.Auxio.App" - android:dataExtractionRules="@xml/data_extraction_rules" android:appCategory="audio" android:enableOnBackInvokedCallback="true" tools:ignore="UnusedAttribute"> diff --git a/app/src/main/res/xml/backup_descriptor.xml b/app/src/main/res/xml/backup_descriptor.xml index 1b0854f7d..587567dab 100644 --- a/app/src/main/res/xml/backup_descriptor.xml +++ b/app/src/main/res/xml/backup_descriptor.xml @@ -1,2 +1,5 @@ - + + + + diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml index 40ee2c3f7..a95e572f9 100644 --- a/app/src/main/res/xml/data_extraction_rules.xml +++ b/app/src/main/res/xml/data_extraction_rules.xml @@ -1,5 +1,10 @@ - - + + + + + + + \ No newline at end of file From 927c4a056eb970844be3e02d7f3f4fd6568aa28a Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Wed, 7 Jun 2023 15:15:01 -0600 Subject: [PATCH 03/26] music: recognize (album)artistssort tags Recognize albumartistssort and artistssort tags, which are apparently written by beets. --- CHANGELOG.md | 3 +++ .../oxycblt/auxio/music/metadata/TagWorker.kt | 20 +++++++++---------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8281d98b..b5ff5f568 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## dev +#### What's Improved +- Tags formatted as `artistssort` or `albumartistssort` are now recognized by Auxio + #### What's Fixed - Disc number is no longer mis-aligned when no subtitle is present diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt index b709cb558..6f6cebda0 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt @@ -146,9 +146,8 @@ private class TagWorkerImpl( // Artist textFrames["TXXX:musicbrainz artist id"]?.let { rawSong.artistMusicBrainzIds = it } (textFrames["TXXX:artists"] ?: textFrames["TPE1"])?.let { rawSong.artistNames = it } - (textFrames["TXXX:artists_sort"] ?: textFrames["TSOP"])?.let { - rawSong.artistSortNames = it - } + (textFrames["TXXX:artistssort"] ?: textFrames["TXXX:artists_sort"] ?: textFrames["TSOP"]) + ?.let { rawSong.artistSortNames = it } // Album artist textFrames["TXXX:musicbrainz album artist id"]?.let { @@ -157,9 +156,9 @@ private class TagWorkerImpl( (textFrames["TXXX:albumartists"] ?: textFrames["TPE2"])?.let { rawSong.albumArtistNames = it } - (textFrames["TXXX:albumartists_sort"] ?: textFrames["TSO2"])?.let { - rawSong.albumArtistSortNames = it - } + (textFrames["TXXX:albumartistssort"] ?: textFrames["TXXX:albumartists_sort"] + ?: textFrames["TSO2"]) + ?.let { rawSong.albumArtistSortNames = it } // Genre textFrames["TCON"]?.let { rawSong.genreNames = it } @@ -249,14 +248,15 @@ private class TagWorkerImpl( // Artist comments["musicbrainz_artistid"]?.let { rawSong.artistMusicBrainzIds = it } (comments["artists"] ?: comments["artist"])?.let { rawSong.artistNames = it } - (comments["artists_sort"] ?: comments["artistsort"])?.let { rawSong.artistSortNames = it } + (comments["artistssort"] ?: comments["artists_sort"] ?: comments["artistsort"] )?.let { + rawSong.artistSortNames = it + } // Album artist comments["musicbrainz_albumartistid"]?.let { rawSong.albumArtistMusicBrainzIds = it } (comments["albumartists"] ?: comments["albumartist"])?.let { rawSong.albumArtistNames = it } - (comments["albumartists_sort"] ?: comments["albumartistsort"])?.let { - rawSong.albumArtistSortNames = it - } + (comments["albumartistssort"] ?: comments["albumartists_sort"] ?: comments["albumartistsort"]) + ?.let { rawSong.albumArtistSortNames = it } // Genre comments["genre"]?.let { rawSong.genreNames = it } From 8edfcd22c723ce18bd592eb08f75076e316c1123 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Wed, 7 Jun 2023 15:15:51 -0600 Subject: [PATCH 04/26] music: make grouping mechanism consistent Actually bother to make the way music is grouped consistent, based on: - The first track for albums - The earliest album for artists - The first song for genres --- .../auxio/music/device/DeviceLibrary.kt | 92 ++++++++++++++++--- .../oxycblt/auxio/music/device/RawMusic.kt | 42 ++++----- 2 files changed, 100 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt index 814c15818..6d2c66899 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt @@ -21,8 +21,8 @@ package org.oxycblt.auxio.music.device import android.content.Context import android.net.Uri import android.provider.OpenableColumns +import java.util.LinkedList import javax.inject.Inject -import org.oxycblt.auxio.list.Sort import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Genre @@ -33,6 +33,7 @@ import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.fs.contentResolverSafe import org.oxycblt.auxio.music.fs.useQuery import org.oxycblt.auxio.util.logD +import org.oxycblt.auxio.util.logW /** * Organized music library information obtained from device storage. @@ -159,19 +160,44 @@ private class DeviceLibraryImpl(rawSongs: List, settings: MusicSettings private fun buildSongs(rawSongs: List, settings: MusicSettings): List { val start = System.currentTimeMillis() - val songs = - Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING) - .songs(rawSongs.map { SongImpl(it, settings) }.distinctBy { it.uid }) + val uidSet = LinkedHashSet(rawSongs.size) + val songs = LinkedList() + for (rawSong in rawSongs) { + val song = SongImpl(rawSong, settings) + if (uidSet.add(song.uid)) { + songs.add(song) + } else { + logW("Duplicate song found: $song") + } + } logD("Successfully built ${songs.size} songs in ${System.currentTimeMillis() - start}ms") return songs } private fun buildAlbums(songs: List, settings: MusicSettings): List { val start = System.currentTimeMillis() + val albumGrouping = mutableMapOf>() + for (song in songs) { + val key = RawAlbum.Key(song.rawAlbum) + val body = albumGrouping[key] + if (body != null) { + body.music.add(song) + val dominantSong = body.dominantRaw.derived + val dominates = + song.track != null && + (dominantSong.track == null || song.track < dominantSong.track) + if (dominates) { + body.dominantRaw = DominantRaw(song.rawAlbum, song) + } + } else { + albumGrouping[key] = Grouping(DominantRaw(song.rawAlbum, song), mutableListOf(song)) + } + } + // Group songs by their singular raw album, then map the raw instances and their // grouped songs to Album values. Album.Raw will handle the actual grouping rules. - val songsByAlbum = songs.groupBy { it.rawAlbum.key } - val albums = songsByAlbum.map { AlbumImpl(it.key.value, settings, it.value) } + val albums = + albumGrouping.values.map { AlbumImpl(it.dominantRaw.inner, settings, it.music) } logD("Successfully built ${albums.size} albums in ${System.currentTimeMillis() - start}ms") return albums } @@ -185,22 +211,44 @@ private class DeviceLibraryImpl(rawSongs: List, settings: MusicSettings // Add every raw artist credited to each Song/Album to the grouping. This way, // different multi-artist combinations are not treated as different artists. // Songs and albums are grouped by artist and album artist respectively. - val musicByArtist = mutableMapOf>() + val artistGrouping = mutableMapOf>() for (song in songs) { for (rawArtist in song.rawArtists) { - musicByArtist.getOrPut(rawArtist.key) { mutableListOf() }.add(song) + val key = RawArtist.Key(rawArtist) + val body = artistGrouping[key] + if (body != null) { + body.music.add(song) + } else { + artistGrouping[key] = + Grouping(DominantRaw(rawArtist, albums.first()), mutableListOf(song)) + } } } for (album in albums) { for (rawArtist in album.rawArtists) { - musicByArtist.getOrPut(rawArtist.key) { mutableListOf() }.add(album) + val key = RawArtist.Key(rawArtist) + val body = artistGrouping[key] + if (body != null) { + body.music.add(album) + val dominantAlbum = body.dominantRaw.derived + val dominates = + album.dates != null && + (dominantAlbum.dates == null || album.dates < dominantAlbum.dates) + if (dominates) { + body.dominantRaw = DominantRaw(rawArtist, album) + } + } else { + artistGrouping[key] = + Grouping(DominantRaw(rawArtist, album), mutableListOf(album)) + } } } // Convert the combined mapping into artist instances. - val artists = musicByArtist.map { ArtistImpl(it.key.value, settings, it.value) } + val artists = + artistGrouping.values.map { ArtistImpl(it.dominantRaw.inner, settings, it.music) } logD( "Successfully built ${artists.size} artists in ${System.currentTimeMillis() - start}ms") return artists @@ -210,16 +258,34 @@ private class DeviceLibraryImpl(rawSongs: List, settings: MusicSettings val start = System.currentTimeMillis() // Add every raw genre credited to each Song to the grouping. This way, // different multi-genre combinations are not treated as different genres. - val songsByGenre = mutableMapOf>() + val songsByGenre = mutableMapOf>() for (song in songs) { for (rawGenre in song.rawGenres) { - songsByGenre.getOrPut(rawGenre.key) { mutableListOf() }.add(song) + val key = RawGenre.Key(rawGenre) + val body = songsByGenre[key] + if (body != null) { + body.music.add(song) + val dominantSong = body.dominantRaw.derived + if (song.date != null && song.name < dominantSong.name) { + body.dominantRaw = DominantRaw(rawGenre, song) + } + } else { + songsByGenre[key] = Grouping(DominantRaw(rawGenre, song), mutableListOf(song)) + } } } // Convert the mapping into genre instances. - val genres = songsByGenre.map { GenreImpl(it.key.value, settings, it.value) } + val genres = + songsByGenre.map { GenreImpl(it.value.dominantRaw.inner, settings, it.value.music) } logD("Successfully built ${genres.size} genres in ${System.currentTimeMillis() - start}ms") return genres } + + data class DominantRaw(val inner: R, val derived: M) + + data class Grouping( + var dominantRaw: DominantRaw, + val music: MutableList + ) } diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt b/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt index 2a198c687..46c84fc51 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt @@ -116,7 +116,7 @@ data class RawAlbum( val key = Key(this) /** Exposed information that denotes [RawAlbum] uniqueness. */ - data class Key(val value: RawAlbum) { + data class Key(private val inner: RawAlbum) { // Albums are grouped as follows: // - If we have a MusicBrainz ID, only group by it. This allows different Albums with the // same name to be differentiated, which is common in large libraries. @@ -126,19 +126,19 @@ data class RawAlbum( // Cache the hash-code for HashMap efficiency. private val hashCode = - value.musicBrainzId?.hashCode() - ?: (31 * value.name.lowercase().hashCode() + value.rawArtists.hashCode()) + inner.musicBrainzId?.hashCode() + ?: (31 * inner.name.lowercase().hashCode() + inner.rawArtists.hashCode()) override fun hashCode() = hashCode override fun equals(other: Any?) = other is Key && when { - value.musicBrainzId != null && other.value.musicBrainzId != null -> - value.musicBrainzId == other.value.musicBrainzId - value.musicBrainzId == null && other.value.musicBrainzId == null -> - other.value.name.equals(other.value.name, true) && - other.value.rawArtists == other.value.rawArtists + inner.musicBrainzId != null && other.inner.musicBrainzId != null -> + inner.musicBrainzId == other.inner.musicBrainzId + inner.musicBrainzId == null && other.inner.musicBrainzId == null -> + inner.name.equals(other.inner.name, true) && + inner.rawArtists == other.inner.rawArtists else -> false } } @@ -164,7 +164,7 @@ data class RawArtist( * Allows [RawArtist]s to be compared by "fundamental" information that is unlikely to change on * an item-by-item */ - data class Key(val value: RawArtist) { + data class Key(private val inner: RawArtist) { // Artists are grouped as follows: // - If we have a MusicBrainz ID, only group by it. This allows different Artists with the // same name to be differentiated, which is common in large libraries. @@ -172,7 +172,7 @@ data class RawArtist( // grouping to be case-insensitive. // Cache the hashCode for HashMap efficiency. - private val hashCode = value.musicBrainzId?.hashCode() ?: value.name?.lowercase().hashCode() + private val hashCode = inner.musicBrainzId?.hashCode() ?: inner.name?.lowercase().hashCode() // Compare names and MusicBrainz IDs in order to differentiate artists with the // same name in large libraries. @@ -182,13 +182,13 @@ data class RawArtist( override fun equals(other: Any?) = other is Key && when { - value.musicBrainzId != null && other.value.musicBrainzId != null -> - value.musicBrainzId == other.value.musicBrainzId - value.musicBrainzId == null && other.value.musicBrainzId == null -> + inner.musicBrainzId != null && other.inner.musicBrainzId != null -> + inner.musicBrainzId == other.inner.musicBrainzId + inner.musicBrainzId == null && other.inner.musicBrainzId == null -> when { - value.name != null && other.value.name != null -> - value.name.equals(other.value.name, true) - value.name == null && other.value.name == null -> true + inner.name != null && other.inner.name != null -> + inner.name.equals(other.inner.name, true) + inner.name == null && other.inner.name == null -> true else -> false } else -> false @@ -207,9 +207,9 @@ data class RawGenre( ) { val key = Key(this) - data class Key(val value: RawGenre) { + data class Key(private val inner: RawGenre) { // Cache the hashCode for HashMap efficiency. - private val hashCode = value.name?.lowercase().hashCode() + private val hashCode = inner.name?.lowercase().hashCode() // Only group by the lowercase genre name. This allows Genre grouping to be // case-insensitive, which may be helpful in some libraries with different ways of @@ -219,9 +219,9 @@ data class RawGenre( override fun equals(other: Any?) = other is Key && when { - value.name != null && other.value.name != null -> - value.name.equals(other.value.name, true) - value.name == null && other.value.name == null -> true + inner.name != null && other.inner.name != null -> + inner.name.equals(other.inner.name, true) + inner.name == null && other.inner.name == null -> true else -> false } } From 07eefda67aa65f944531a1874b7e89a0453ec3dc Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Wed, 7 Jun 2023 19:32:22 -0600 Subject: [PATCH 05/26] music: parallelize library creation Make it so that the DeviceLibrary constructor streams all song information instead of building the library on completion. This has no measurable effect on loading times, but does appear visibly faster to the user since the loading process is no longer stuck on the "Loading your music library" step. --- .../oxycblt/auxio/music/MusicRepository.kt | 34 +- .../auxio/music/device/DeviceLibrary.kt | 323 +++++++++--------- .../oxycblt/auxio/music/metadata/TagWorker.kt | 9 +- .../oxycblt/auxio/music/user/UserLibrary.kt | 40 +-- .../org/oxycblt/auxio/search/SearchEngine.kt | 12 +- 5 files changed, 205 insertions(+), 213 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt index 8d890b218..f1da906b7 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -372,6 +372,7 @@ constructor( // Do the initial query of the cache and media databases in parallel. logD("Starting MediaStore query") val mediaStoreQueryJob = worker.scope.tryAsync { mediaStoreExtractor.query() } + val userLibraryQueryJob = worker.scope.tryAsync { userLibraryFactory.query() } val cache = if (withCache) { logD("Reading cache") @@ -388,6 +389,7 @@ constructor( logD("Starting song discovery") val completeSongs = Channel(Channel.UNLIMITED) val incompleteSongs = Channel(Channel.UNLIMITED) + val processedSongs = Channel(Channel.UNLIMITED) logD("Started MediaStore discovery") val mediaStoreJob = worker.scope.tryAsync { @@ -400,10 +402,17 @@ constructor( tagExtractor.consume(incompleteSongs, completeSongs) completeSongs.close() } + logD("Starting DeviceLibrary creation") + val deviceLibraryJob = + worker.scope.tryAsync(Dispatchers.Default) { + deviceLibraryFactory.create(completeSongs, processedSongs).also { + processedSongs.close() + } + } // Await completed raw songs as they are processed. val rawSongs = LinkedList() - for (rawSong in completeSongs) { + for (rawSong in processedSongs) { rawSongs.add(rawSong) emitLoading(IndexingProgress.Songs(rawSongs.size, query.projectedTotal)) } @@ -417,29 +426,22 @@ constructor( throw NoMusicException() } - // Successfully loaded the library, now save the cache, create the library, and - // read playlist information in parallel. + // Successfully loaded the library, now save the cache and read playlist information + // in parallel. logD("Discovered ${rawSongs.size} songs, starting finalization") // TODO: Indicate playlist state in loading process? emitLoading(IndexingProgress.Indeterminate) - val deviceLibraryChannel = Channel() - logD("Starting DeviceLibrary creation") - val deviceLibraryJob = - worker.scope.tryAsync(Dispatchers.Default) { - deviceLibraryFactory.create(rawSongs).also { deviceLibraryChannel.send(it) } - } - logD("Starting UserLibrary creation") - val userLibraryJob = - worker.scope.tryAsync { - userLibraryFactory.read(deviceLibraryChannel).also { deviceLibraryChannel.close() } - } + logD("Starting UserLibrary query") if (cache == null || cache.invalidated) { logD("Writing cache [why=${cache?.invalidated}]") cacheRepository.writeCache(rawSongs) } - logD("Awaiting library creation") + logD("Awaiting UserLibrary query") + val rawPlaylists = userLibraryQueryJob.await().getOrThrow() + logD("Awaiting DeviceLibrary creation") val deviceLibrary = deviceLibraryJob.await().getOrThrow() - val userLibrary = userLibraryJob.await().getOrThrow() + logD("Starting UserLibrary creation") + val userLibrary = userLibraryFactory.create(rawPlaylists, deviceLibrary) logD("Successfully indexed music library [device=$deviceLibrary user=$userLibrary]") emitComplete(null) diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt index 6d2c66899..f53a87ea2 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt @@ -21,8 +21,8 @@ package org.oxycblt.auxio.music.device import android.content.Context import android.net.Uri import android.provider.OpenableColumns -import java.util.LinkedList import javax.inject.Inject +import kotlinx.coroutines.channels.Channel import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Genre @@ -32,8 +32,8 @@ import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.fs.contentResolverSafe import org.oxycblt.auxio.music.fs.useQuery -import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logW +import org.oxycblt.auxio.util.unlikelyToBeNull /** * Organized music library information obtained from device storage. @@ -46,13 +46,13 @@ import org.oxycblt.auxio.util.logW */ interface DeviceLibrary { /** All [Song]s in this [DeviceLibrary]. */ - val songs: List + val songs: Collection /** All [Album]s in this [DeviceLibrary]. */ - val albums: List + val albums: Collection /** All [Artist]s in this [DeviceLibrary]. */ - val artists: List + val artists: Collection /** All [Genre]s in this [DeviceLibrary]. */ - val genres: List + val genres: Collection /** * Find a [Song] instance corresponding to the given [Music.UID]. @@ -97,38 +97,157 @@ interface DeviceLibrary { /** Constructs a [DeviceLibrary] implementation in an asynchronous manner. */ interface Factory { - /** - * Create a new [DeviceLibrary]. - * - * @param rawSongs [RawSong] instances to create a [DeviceLibrary] from. - */ - suspend fun create(rawSongs: List): DeviceLibrary - } - - companion object { - /** - * Create an instance of [DeviceLibrary]. - * - * @param rawSongs [RawSong]s to create the library out of. - * @param settings [MusicSettings] required. - */ - fun from(rawSongs: List, settings: MusicSettings): DeviceLibrary = - DeviceLibraryImpl(rawSongs, settings) + suspend fun create( + rawSongs: Channel, + processedSongs: Channel + ): DeviceLibraryImpl } } class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: MusicSettings) : DeviceLibrary.Factory { - override suspend fun create(rawSongs: List): DeviceLibrary = - DeviceLibraryImpl(rawSongs, musicSettings) + override suspend fun create( + rawSongs: Channel, + processedSongs: Channel + ): DeviceLibraryImpl { + val songGrouping = mutableMapOf() + val albumGrouping = mutableMapOf>() + val artistGrouping = mutableMapOf>() + val genreGrouping = mutableMapOf>() + + // All music information is grouped as it is indexed by other components. + for (rawSong in rawSongs) { + val song = SongImpl(rawSong, musicSettings) + // At times the indexer produces duplicate songs, try to filter these. Comparing by + // UID is sufficient for something like this, and also prevents collisions from + // causing severe issues elsewhere. + if (songGrouping.containsKey(song.uid)) { + logW( + "Duplicate song found: ${song.path} in " + + "collides with ${unlikelyToBeNull(songGrouping[song.uid]).path}") + processedSongs.send(rawSong) + continue + } + songGrouping[song.uid] = song + + // Group the new song into an album. + val albumKey = song.rawAlbum.key + val albumBody = albumGrouping[albumKey] + if (albumBody != null) { + albumBody.music.add(song) + val prioritized = albumBody.raw.src + // Since albums are grouped fuzzily, we pick the song with the earliest track to + // use for album information to ensure consistent metadata and UIDs. Fall back to + // the name otherwise. + val trackLower = + song.track != null && (prioritized.track == null || song.track < prioritized.track) + val nameLower = + song.name < prioritized.name + if (trackLower || nameLower) { + albumBody.raw = PrioritizedRaw(song.rawAlbum, song) + } + } else { + // Need to initialize this grouping. + albumGrouping[albumKey] = + Grouping(PrioritizedRaw(song.rawAlbum, song), mutableListOf(song)) + } + + // Group the song into each of it's artists. + for (rawArtist in song.rawArtists) { + val artistKey = rawArtist.key + val artistBody = artistGrouping[artistKey] + if (artistBody != null) { + // Since artists are not guaranteed to have songs, song artist information is + // de-prioritized compared to album artist information. + artistBody.music.add(song) + } else { + // Need to initialize this grouping. + artistGrouping[artistKey] = + Grouping(PrioritizedRaw(rawArtist, song), mutableListOf(song)) + } + } + + // Group the song into each of it's genres. + for (rawGenre in song.rawGenres) { + val genreKey = rawGenre.key + val genreBody = genreGrouping[genreKey] + if (genreBody != null) { + genreBody.music.add(song) + // Genre information from higher songs in ascending alphabetical order are + // prioritized. + val prioritized = genreBody.raw.src + val nameLower = song.name < prioritized.name + if (nameLower) { + genreBody.raw = PrioritizedRaw(rawGenre, song) + } + } else { + // Need to initialize this grouping. + genreGrouping[genreKey] = + Grouping(PrioritizedRaw(rawGenre, song), mutableListOf(song)) + } + } + + processedSongs.send(rawSong) + } + + // Now that all songs are processed, also process albums and group them into their + // respective artists. + val albums = + albumGrouping.values.map { AlbumImpl(it.raw.inner, musicSettings, it.music) } + for (album in albums) { + for (rawArtist in album.rawArtists) { + val key = RawArtist.Key(rawArtist) + val body = artistGrouping[key] + if (body != null) { + body.music.add(album) + when (val prioritized = body.raw.src) { + // Immediately replace any songs that initially held the priority position. + is SongImpl -> body.raw = PrioritizedRaw(rawArtist, album) + is AlbumImpl -> { + // Album information from later dates is prioritized, as it is more likely to + // contain the "modern" name of the artist if the information really is + // in-consistent. Fall back to the name otherwise. + val dateEarlier = + album.dates != null && (prioritized.dates == null || album.dates < prioritized.dates) + val nameLower = + album.name < prioritized.name + if (dateEarlier || nameLower) { + body.raw = PrioritizedRaw(rawArtist, album) + } + } + else -> throw IllegalStateException() + } + } else { + // Need to initialize this grouping. + artistGrouping[key] = + Grouping(PrioritizedRaw(rawArtist, album), mutableListOf(album)) + } + } + } + + // Artists and genres do not need to be grouped and can be processed immediately. + val artists = + artistGrouping.values.map { ArtistImpl(it.raw.inner, musicSettings, it.music) } + val genres = + genreGrouping.values.map { GenreImpl(it.raw.inner, musicSettings, it.music) } + + return DeviceLibraryImpl(songGrouping.values, albums, artists, genres) + } + + private data class Grouping( + var raw: PrioritizedRaw, + val music: MutableList + ) + + private data class PrioritizedRaw(val inner: R, val src: M) } -private class DeviceLibraryImpl(rawSongs: List, settings: MusicSettings) : DeviceLibrary { - override val songs = buildSongs(rawSongs, settings) - override val albums = buildAlbums(songs, settings) - override val artists = buildArtists(songs, albums, settings) - override val genres = buildGenres(songs, settings) - +class DeviceLibraryImpl( + override val songs: Collection, + override val albums: Collection, + override val artists: Collection, + override val genres: Collection +) : DeviceLibrary { // Use a mapping to make finding information based on it's UID much faster. private val songUidMap = buildMap { songs.forEach { put(it.uid, it.finalize()) } } private val albumUidMap = buildMap { albums.forEach { put(it.uid, it.finalize()) } } @@ -139,12 +258,13 @@ private class DeviceLibraryImpl(rawSongs: List, settings: MusicSettings override fun equals(other: Any?) = other is DeviceLibrary && other.songs == songs override fun hashCode() = songs.hashCode() override fun toString() = - "DeviceLibrary(songs=${songs.size}, albums=${albums.size}, artists=${artists.size}, genres=${genres.size})" + "DeviceLibrary(songs=${songs.size}, albums=${albums.size}, " + + "artists=${artists.size}, genres=${genres.size})" - override fun findSong(uid: Music.UID) = songUidMap[uid] - override fun findAlbum(uid: Music.UID) = albumUidMap[uid] - override fun findArtist(uid: Music.UID) = artistUidMap[uid] - override fun findGenre(uid: Music.UID) = genreUidMap[uid] + override fun findSong(uid: Music.UID): Song? = songUidMap[uid] + override fun findAlbum(uid: Music.UID): Album? = albumUidMap[uid] + override fun findArtist(uid: Music.UID): Artist? = artistUidMap[uid] + override fun findGenre(uid: Music.UID): Genre? = genreUidMap[uid] override fun findSongForUri(context: Context, uri: Uri) = context.contentResolverSafe.useQuery( @@ -157,135 +277,4 @@ private class DeviceLibraryImpl(rawSongs: List, settings: MusicSettings val size = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE)) songs.find { it.path.name == displayName && it.size == size } } - - private fun buildSongs(rawSongs: List, settings: MusicSettings): List { - val start = System.currentTimeMillis() - val uidSet = LinkedHashSet(rawSongs.size) - val songs = LinkedList() - for (rawSong in rawSongs) { - val song = SongImpl(rawSong, settings) - if (uidSet.add(song.uid)) { - songs.add(song) - } else { - logW("Duplicate song found: $song") - } - } - logD("Successfully built ${songs.size} songs in ${System.currentTimeMillis() - start}ms") - return songs - } - - private fun buildAlbums(songs: List, settings: MusicSettings): List { - val start = System.currentTimeMillis() - val albumGrouping = mutableMapOf>() - for (song in songs) { - val key = RawAlbum.Key(song.rawAlbum) - val body = albumGrouping[key] - if (body != null) { - body.music.add(song) - val dominantSong = body.dominantRaw.derived - val dominates = - song.track != null && - (dominantSong.track == null || song.track < dominantSong.track) - if (dominates) { - body.dominantRaw = DominantRaw(song.rawAlbum, song) - } - } else { - albumGrouping[key] = Grouping(DominantRaw(song.rawAlbum, song), mutableListOf(song)) - } - } - - // Group songs by their singular raw album, then map the raw instances and their - // grouped songs to Album values. Album.Raw will handle the actual grouping rules. - val albums = - albumGrouping.values.map { AlbumImpl(it.dominantRaw.inner, settings, it.music) } - logD("Successfully built ${albums.size} albums in ${System.currentTimeMillis() - start}ms") - return albums - } - - private fun buildArtists( - songs: List, - albums: List, - settings: MusicSettings - ): List { - val start = System.currentTimeMillis() - // Add every raw artist credited to each Song/Album to the grouping. This way, - // different multi-artist combinations are not treated as different artists. - // Songs and albums are grouped by artist and album artist respectively. - val artistGrouping = mutableMapOf>() - - for (song in songs) { - for (rawArtist in song.rawArtists) { - val key = RawArtist.Key(rawArtist) - val body = artistGrouping[key] - if (body != null) { - body.music.add(song) - } else { - artistGrouping[key] = - Grouping(DominantRaw(rawArtist, albums.first()), mutableListOf(song)) - } - } - } - - for (album in albums) { - for (rawArtist in album.rawArtists) { - val key = RawArtist.Key(rawArtist) - val body = artistGrouping[key] - if (body != null) { - body.music.add(album) - val dominantAlbum = body.dominantRaw.derived - val dominates = - album.dates != null && - (dominantAlbum.dates == null || album.dates < dominantAlbum.dates) - if (dominates) { - body.dominantRaw = DominantRaw(rawArtist, album) - } - } else { - artistGrouping[key] = - Grouping(DominantRaw(rawArtist, album), mutableListOf(album)) - } - } - } - - // Convert the combined mapping into artist instances. - val artists = - artistGrouping.values.map { ArtistImpl(it.dominantRaw.inner, settings, it.music) } - logD( - "Successfully built ${artists.size} artists in ${System.currentTimeMillis() - start}ms") - return artists - } - - private fun buildGenres(songs: List, settings: MusicSettings): List { - val start = System.currentTimeMillis() - // Add every raw genre credited to each Song to the grouping. This way, - // different multi-genre combinations are not treated as different genres. - val songsByGenre = mutableMapOf>() - for (song in songs) { - for (rawGenre in song.rawGenres) { - val key = RawGenre.Key(rawGenre) - val body = songsByGenre[key] - if (body != null) { - body.music.add(song) - val dominantSong = body.dominantRaw.derived - if (song.date != null && song.name < dominantSong.name) { - body.dominantRaw = DominantRaw(rawGenre, song) - } - } else { - songsByGenre[key] = Grouping(DominantRaw(rawGenre, song), mutableListOf(song)) - } - } - } - - // Convert the mapping into genre instances. - val genres = - songsByGenre.map { GenreImpl(it.value.dominantRaw.inner, settings, it.value.music) } - logD("Successfully built ${genres.size} genres in ${System.currentTimeMillis() - start}ms") - return genres - } - - data class DominantRaw(val inner: R, val derived: M) - - data class Grouping( - var dominantRaw: DominantRaw, - val music: MutableList - ) } diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt index 6f6cebda0..e1501bede 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt @@ -156,8 +156,8 @@ private class TagWorkerImpl( (textFrames["TXXX:albumartists"] ?: textFrames["TPE2"])?.let { rawSong.albumArtistNames = it } - (textFrames["TXXX:albumartistssort"] ?: textFrames["TXXX:albumartists_sort"] - ?: textFrames["TSO2"]) + (textFrames["TXXX:albumartistssort"] + ?: textFrames["TXXX:albumartists_sort"] ?: textFrames["TSO2"]) ?.let { rawSong.albumArtistSortNames = it } // Genre @@ -248,14 +248,15 @@ private class TagWorkerImpl( // Artist comments["musicbrainz_artistid"]?.let { rawSong.artistMusicBrainzIds = it } (comments["artists"] ?: comments["artist"])?.let { rawSong.artistNames = it } - (comments["artistssort"] ?: comments["artists_sort"] ?: comments["artistsort"] )?.let { + (comments["artistssort"] ?: comments["artists_sort"] ?: comments["artistsort"])?.let { rawSong.artistSortNames = it } // Album artist comments["musicbrainz_albumartistid"]?.let { rawSong.albumArtistMusicBrainzIds = it } (comments["albumartists"] ?: comments["albumartist"])?.let { rawSong.albumArtistNames = it } - (comments["albumartistssort"] ?: comments["albumartists_sort"] ?: comments["albumartistsort"]) + (comments["albumartistssort"] + ?: comments["albumartists_sort"] ?: comments["albumartistsort"]) ?.let { rawSong.albumArtistSortNames = it } // Genre diff --git a/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt index 412b14fa4..d6265bd00 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt @@ -20,7 +20,6 @@ package org.oxycblt.auxio.music.user import java.lang.Exception import javax.inject.Inject -import kotlinx.coroutines.channels.Channel import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.MusicSettings @@ -37,6 +36,8 @@ import org.oxycblt.auxio.util.logE * is also not backed by library information, rather an app database with in-memory caching. It is * generally not expected to create this yourself, and instead rely on MusicRepository. * + * TODO: Communicate errors + * * @author Alexander Capehart */ interface UserLibrary { @@ -61,15 +62,12 @@ interface UserLibrary { /** Constructs a [UserLibrary] implementation in an asynchronous manner. */ interface Factory { - /** - * Create a new [UserLibrary]. - * - * @param deviceLibraryChannel Asynchronously populated [DeviceLibrary] that can be obtained - * later. This allows database information to be read before the actual instance is - * constructed. - * @return A new [MutableUserLibrary] with the required implementation. - */ - suspend fun read(deviceLibraryChannel: Channel): MutableUserLibrary + suspend fun query(): List + + suspend fun create( + rawPlaylists: List, + deviceLibrary: DeviceLibrary + ): MutableUserLibrary } } @@ -123,17 +121,19 @@ class UserLibraryFactoryImpl @Inject constructor(private val playlistDao: PlaylistDao, private val musicSettings: MusicSettings) : UserLibrary.Factory { - override suspend fun read(deviceLibraryChannel: Channel): MutableUserLibrary { - // While were waiting for the library, read our playlists out. - val rawPlaylists = - try { - playlistDao.readRawPlaylists() - } catch (e: Exception) { - logE("Unable to read playlists: $e") - return UserLibraryImpl(playlistDao, mutableMapOf(), musicSettings) - } + override suspend fun query() = + try { + playlistDao.readRawPlaylists() + } catch (e: Exception) { + logE("Unable to read playlists: $e") + listOf() + } + + override suspend fun create( + rawPlaylists: List, + deviceLibrary: DeviceLibrary + ): MutableUserLibrary { logD("Successfully read ${rawPlaylists.size} playlists") - val deviceLibrary = deviceLibraryChannel.receive() // Convert the database playlist information to actual usable playlists. val playlistMap = mutableMapOf() for (rawPlaylist in rawPlaylists) { diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchEngine.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchEngine.kt index a4471ae5e..24c5389fd 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchEngine.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchEngine.kt @@ -58,11 +58,11 @@ interface SearchEngine { * @param playlists A list of [Playlist], null if empty. */ data class Items( - val songs: List?, - val albums: List?, - val artists: List?, - val genres: List?, - val playlists: List? + val songs: Collection?, + val albums: Collection?, + val artists: Collection?, + val genres: Collection?, + val playlists: Collection? ) } @@ -90,7 +90,7 @@ class SearchEngineImpl @Inject constructor(@ApplicationContext private val conte * initially. This can be used to compare against additional attributes to improve search * result quality. */ - private inline fun List.searchListImpl( + private inline fun Collection.searchListImpl( query: String, fallback: (String, T) -> Boolean = { _, _ -> false } ) = From 8d97e86c8d6af1c8f231eb347bfc83b22bb5dc4a Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Wed, 7 Jun 2023 19:40:31 -0600 Subject: [PATCH 06/26] music: synchronize userlibrary value reads Use synchronized when reading and writing values in UserLibrary. Convention for all shared objects. --- CHANGELOG.md | 1 + .../oxycblt/auxio/music/MusicRepository.kt | 1 - .../oxycblt/auxio/music/user/UserLibrary.kt | 34 ++++++++++++------- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b5ff5f568..72e2fbf10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ #### What's Improved - Tags formatted as `artistssort` or `albumartistssort` are now recognized by Auxio +- Reduced visual loading time #### What's Fixed - Disc number is no longer mis-aligned when no subtitle is present diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt index f1da906b7..ab8bb16cb 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -429,7 +429,6 @@ constructor( // Successfully loaded the library, now save the cache and read playlist information // in parallel. logD("Discovered ${rawSongs.size} songs, starting finalization") - // TODO: Indicate playlist state in loading process? emitLoading(IndexingProgress.Indeterminate) logD("Starting UserLibrary query") if (cache == null || cache.invalidated) { diff --git a/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt index d6265bd00..a66bfd69a 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt @@ -161,7 +161,6 @@ private class UserLibraryImpl( override fun findPlaylist(name: String) = playlistMap.values.find { it.name.raw == name } override suspend fun createPlaylist(name: String, songs: List) { - // TODO: Use synchronized with value access too val playlistImpl = PlaylistImpl.from(name, songs, musicSettings) synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl } val rawPlaylist = @@ -181,8 +180,11 @@ private class UserLibraryImpl( override suspend fun renamePlaylist(playlist: Playlist, name: String) { val playlistImpl = - requireNotNull(playlistMap[playlist.uid]) { "Cannot rename invalid playlist" } - synchronized(this) { playlistMap[playlist.uid] = playlistImpl.edit(name, musicSettings) } + synchronized(this) { + requireNotNull(playlistMap[playlist.uid]) { "Cannot rename invalid playlist" }.also { + playlistMap[it.uid] = it.edit(name, musicSettings) + } + } try { playlistDao.replacePlaylistInfo(PlaylistInfo(playlist.uid, name)) logD("Successfully renamed $playlist to $name") @@ -195,9 +197,11 @@ private class UserLibraryImpl( } override suspend fun deletePlaylist(playlist: Playlist) { - val playlistImpl = - requireNotNull(playlistMap[playlist.uid]) { "Cannot remove invalid playlist" } - synchronized(this) { playlistMap.remove(playlistImpl.uid) } + val playlistImpl = synchronized(this) { + requireNotNull(playlistMap[playlist.uid]) { "Cannot remove invalid playlist" }.also { + playlistMap.remove(it.uid) + } + } try { playlistDao.deletePlaylist(playlist.uid) logD("Successfully deleted $playlist") @@ -210,9 +214,12 @@ private class UserLibraryImpl( } override suspend fun addToPlaylist(playlist: Playlist, songs: List) { - val playlistImpl = - requireNotNull(playlistMap[playlist.uid]) { "Cannot add to invalid playlist" } - synchronized(this) { playlistMap[playlist.uid] = playlistImpl.edit { addAll(songs) } } + val playlistImpl = synchronized(this) { + requireNotNull(playlistMap[playlist.uid]) { "Cannot add to invalid playlist" }.also { + playlistMap[it.uid] = it.edit { addAll(songs) } + } + } + try { playlistDao.insertPlaylistSongs(playlist.uid, songs.map { PlaylistSong(it.uid) }) logD("Successfully added ${songs.size} songs to $playlist") @@ -225,9 +232,12 @@ private class UserLibraryImpl( } override suspend fun rewritePlaylist(playlist: Playlist, songs: List) { - val playlistImpl = - requireNotNull(playlistMap[playlist.uid]) { "Cannot rewrite invalid playlist" } - synchronized(this) { playlistMap[playlist.uid] = playlistImpl.edit(songs) } + val playlistImpl = synchronized(this) { + requireNotNull(playlistMap[playlist.uid]) { "Cannot rewrite invalid playlist" }.also { + playlistMap[it.uid] = it.edit(songs) + } + } + try { playlistDao.replacePlaylistSongs(playlist.uid, songs.map { PlaylistSong(it.uid) }) logD("Successfully rewrote $playlist with ${songs.size} songs") From 77d01a0c9703e7e90bae5273c0b0d324b7c56f49 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Wed, 7 Jun 2023 20:07:06 -0600 Subject: [PATCH 07/26] home: use constant page limit Use a constant page limit of 5 instead of a dynamic page limit. This was not being properly updated prior, and since the ViewPager already clamps it, the limit does not really need to be based on the tab size anyway. --- app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt | 8 +++++--- app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt index f1f856684..f39a54e1f 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -52,6 +52,7 @@ import org.oxycblt.auxio.home.list.GenreListFragment import org.oxycblt.auxio.home.list.PlaylistListFragment import org.oxycblt.auxio.home.list.SongListFragment import org.oxycblt.auxio.home.tabs.AdaptiveTabStrategy +import org.oxycblt.auxio.home.tabs.Tab import org.oxycblt.auxio.list.Sort import org.oxycblt.auxio.list.selection.SelectionFragment import org.oxycblt.auxio.list.selection.SelectionViewModel @@ -152,9 +153,10 @@ class HomeFragment : setOnApplyWindowInsetsListener { _, insets -> insets } // We know that there will only be a fixed amount of tabs, so we manually set this - // limit to that. This also prevents the appbar lift state from being confused during - // page transitions. - offscreenPageLimit = homeModel.currentTabModes.size + // limit to the maximum amount possible. This will prevent the tab ripple from + // bugging out due to dynamically inflating each fragment, at the cost of slower + // debug UI performance. + offscreenPageLimit = Tab.MAX_SEQUENCE_IDX + 1 // By default, ViewPager2's sensitivity is high enough to result in vertical scroll // events being registered as horizontal scroll events. Reflect into the internal diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt index 2fddd1b4a..5cacd084b 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt @@ -59,7 +59,7 @@ sealed class Tab(open val mode: MusicMode) { // MusicMode for this tab. /** The maximum index that a well-formed tab sequence should be. */ - private const val MAX_SEQUENCE_IDX = 4 + const val MAX_SEQUENCE_IDX = 4 /** * The default tab sequence, in integer form. This represents a set of four visible tabs From a9a6d1ccc1e6d52fc8d4f5448dd50a4618e7e1ae Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Wed, 7 Jun 2023 20:08:57 -0600 Subject: [PATCH 08/26] music: cleanup Clean up parts of the music loader. --- .../oxycblt/auxio/music/MusicRepository.kt | 2 +- .../auxio/music/device/DeviceLibrary.kt | 36 ++++++++++++------- .../oxycblt/auxio/music/user/UserLibrary.kt | 4 +-- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt index ab8bb16cb..6fa6801fb 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -372,7 +372,6 @@ constructor( // Do the initial query of the cache and media databases in parallel. logD("Starting MediaStore query") val mediaStoreQueryJob = worker.scope.tryAsync { mediaStoreExtractor.query() } - val userLibraryQueryJob = worker.scope.tryAsync { userLibraryFactory.query() } val cache = if (withCache) { logD("Reading cache") @@ -431,6 +430,7 @@ constructor( logD("Discovered ${rawSongs.size} songs, starting finalization") emitLoading(IndexingProgress.Indeterminate) logD("Starting UserLibrary query") + val userLibraryQueryJob = worker.scope.tryAsync { userLibraryFactory.query() } if (cache == null || cache.invalidated) { logD("Writing cache [why=${cache?.invalidated}]") cacheRepository.writeCache(rawSongs) diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt index f53a87ea2..08e312ed5 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt @@ -97,6 +97,14 @@ interface DeviceLibrary { /** Constructs a [DeviceLibrary] implementation in an asynchronous manner. */ interface Factory { + /** + * Creates a new [DeviceLibrary] instance asynchronously based on the incoming stream of + * [RawSong] instances. + * + * @param rawSongs A stream of [RawSong] instances to process. + * @param processedSongs A stream of [RawSong] instances that will have been processed by + * the instance. + */ suspend fun create( rawSongs: Channel, processedSongs: Channel @@ -123,8 +131,11 @@ class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: Mu // causing severe issues elsewhere. if (songGrouping.containsKey(song.uid)) { logW( - "Duplicate song found: ${song.path} in " + + "Duplicate song found: ${song.path} " + "collides with ${unlikelyToBeNull(songGrouping[song.uid]).path}") + // We still want to say that we "processed" the song so that the user doesn't + // get confused at why the bar was only partly filled by the end of the loading + // process. processedSongs.send(rawSong) continue } @@ -140,9 +151,9 @@ class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: Mu // use for album information to ensure consistent metadata and UIDs. Fall back to // the name otherwise. val trackLower = - song.track != null && (prioritized.track == null || song.track < prioritized.track) - val nameLower = - song.name < prioritized.name + song.track != null && + (prioritized.track == null || song.track < prioritized.track) + val nameLower = song.name < prioritized.name if (trackLower || nameLower) { albumBody.raw = PrioritizedRaw(song.rawAlbum, song) } @@ -192,8 +203,7 @@ class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: Mu // Now that all songs are processed, also process albums and group them into their // respective artists. - val albums = - albumGrouping.values.map { AlbumImpl(it.raw.inner, musicSettings, it.music) } + val albums = albumGrouping.values.map { AlbumImpl(it.raw.inner, musicSettings, it.music) } for (album in albums) { for (rawArtist in album.rawArtists) { val key = RawArtist.Key(rawArtist) @@ -204,13 +214,14 @@ class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: Mu // Immediately replace any songs that initially held the priority position. is SongImpl -> body.raw = PrioritizedRaw(rawArtist, album) is AlbumImpl -> { - // Album information from later dates is prioritized, as it is more likely to + // Album information from later dates is prioritized, as it is more + // likely to // contain the "modern" name of the artist if the information really is // in-consistent. Fall back to the name otherwise. val dateEarlier = - album.dates != null && (prioritized.dates == null || album.dates < prioritized.dates) - val nameLower = - album.name < prioritized.name + album.dates != null && + (prioritized.dates == null || album.dates < prioritized.dates) + val nameLower = album.name < prioritized.name if (dateEarlier || nameLower) { body.raw = PrioritizedRaw(rawArtist, album) } @@ -228,8 +239,7 @@ class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: Mu // Artists and genres do not need to be grouped and can be processed immediately. val artists = artistGrouping.values.map { ArtistImpl(it.raw.inner, musicSettings, it.music) } - val genres = - genreGrouping.values.map { GenreImpl(it.raw.inner, musicSettings, it.music) } + val genres = genreGrouping.values.map { GenreImpl(it.raw.inner, musicSettings, it.music) } return DeviceLibraryImpl(songGrouping.values, albums, artists, genres) } @@ -259,7 +269,7 @@ class DeviceLibraryImpl( override fun hashCode() = songs.hashCode() override fun toString() = "DeviceLibrary(songs=${songs.size}, albums=${albums.size}, " + - "artists=${artists.size}, genres=${genres.size})" + "artists=${artists.size}, genres=${genres.size})" override fun findSong(uid: Music.UID): Song? = songUidMap[uid] override fun findAlbum(uid: Music.UID): Album? = albumUidMap[uid] diff --git a/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt index a66bfd69a..1be916cf0 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt @@ -36,9 +36,9 @@ import org.oxycblt.auxio.util.logE * is also not backed by library information, rather an app database with in-memory caching. It is * generally not expected to create this yourself, and instead rely on MusicRepository. * - * TODO: Communicate errors - * * @author Alexander Capehart + * + * TODO: Communicate errors */ interface UserLibrary { /** The current user-defined playlists. */ From 5ab46ba5d17fd3024c7b68696c098db85043b17c Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Wed, 7 Jun 2023 20:09:08 -0600 Subject: [PATCH 09/26] music: add userlibrary error returns Make UserLibrary return some kind of error indicator if something fails. I don't have the framework for how the app will display these errors just yet. --- app/build.gradle | 2 +- .../oxycblt/auxio/music/user/UserLibrary.kt | 97 ++++++++++++------- 2 files changed, 63 insertions(+), 36 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index cc1075aad..bd85eca83 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,6 +56,7 @@ android { } } } + packagingOptions { jniLibs { excludes += ['**/kotlin/**', '**/okhttp3/**'] @@ -65,7 +66,6 @@ android { } } - buildFeatures { viewBinding true } diff --git a/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt index 1be916cf0..0228764a3 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt @@ -62,8 +62,22 @@ interface UserLibrary { /** Constructs a [UserLibrary] implementation in an asynchronous manner. */ interface Factory { + /** + * Read all [RawPlaylist] information from the database, which can be transformed into a + * [UserLibrary] later. + * + * @return A list of [RawPlaylist]s. + */ suspend fun query(): List + /** + * Create a new [UserLibrary] from read [RawPlaylist] instances and a precursor + * [DeviceLibrary]. + * + * @param rawPlaylists The [RawPlaylist]s to use. + * @param deviceLibrary The [DeviceLibrary] to use. + * @return The new [UserLibrary] instance. + */ suspend fun create( rawPlaylists: List, deviceLibrary: DeviceLibrary @@ -83,38 +97,44 @@ interface MutableUserLibrary : UserLibrary { * * @param name The name of the [Playlist]. * @param songs The songs to place in the [Playlist]. + * @return The new [Playlist] instance, or null if one could not be created. */ - suspend fun createPlaylist(name: String, songs: List) + suspend fun createPlaylist(name: String, songs: List): Playlist? /** * Rename a [Playlist]. * * @param playlist The [Playlist] to rename. * @param name The name of the new [Playlist]. + * @return True if the [Playlist] was successfully renamed, false otherwise. */ - suspend fun renamePlaylist(playlist: Playlist, name: String) + suspend fun renamePlaylist(playlist: Playlist, name: String): Boolean /** * Delete a [Playlist]. * * @param playlist The playlist to delete. + * @return True if the [Playlist] was successfully deleted, false otherwise. */ - suspend fun deletePlaylist(playlist: Playlist) + suspend fun deletePlaylist(playlist: Playlist): Boolean /** * Add [Song]s to a [Playlist]. * * @param playlist The [Playlist] to add to. Must currently exist. + * @param songs The [Song]s to add to the [Playlist]. + * @return True if the [Song]s were successfully added, false otherwise. */ - suspend fun addToPlaylist(playlist: Playlist, songs: List) + suspend fun addToPlaylist(playlist: Playlist, songs: List): Boolean /** * Update the [Song]s of a [Playlist]. * * @param playlist The [Playlist] to update. * @param songs The new [Song]s to be contained in the [Playlist]. + * @return True if the [Playlist] was successfully updated, false otherwise. */ - suspend fun rewritePlaylist(playlist: Playlist, songs: List) + suspend fun rewritePlaylist(playlist: Playlist, songs: List): Boolean } class UserLibraryFactoryImpl @@ -160,92 +180,99 @@ private class UserLibraryImpl( override fun findPlaylist(name: String) = playlistMap.values.find { it.name.raw == name } - override suspend fun createPlaylist(name: String, songs: List) { + override suspend fun createPlaylist(name: String, songs: List): Playlist? { val playlistImpl = PlaylistImpl.from(name, songs, musicSettings) synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl } val rawPlaylist = RawPlaylist( PlaylistInfo(playlistImpl.uid, playlistImpl.name.raw), playlistImpl.songs.map { PlaylistSong(it.uid) }) - try { + + return try { playlistDao.insertPlaylist(rawPlaylist) logD("Successfully created playlist $name with ${songs.size} songs") + playlistImpl } catch (e: Exception) { logE("Unable to create playlist $name with ${songs.size} songs") logE(e.stackTraceToString()) synchronized(this) { playlistMap.remove(playlistImpl.uid) } - return + null } } - override suspend fun renamePlaylist(playlist: Playlist, name: String) { + override suspend fun renamePlaylist(playlist: Playlist, name: String): Boolean { val playlistImpl = synchronized(this) { - requireNotNull(playlistMap[playlist.uid]) { "Cannot rename invalid playlist" }.also { - playlistMap[it.uid] = it.edit(name, musicSettings) - } + requireNotNull(playlistMap[playlist.uid]) { "Cannot rename invalid playlist" } + .also { playlistMap[it.uid] = it.edit(name, musicSettings) } } - try { + + return try { playlistDao.replacePlaylistInfo(PlaylistInfo(playlist.uid, name)) logD("Successfully renamed $playlist to $name") + true } catch (e: Exception) { logE("Unable to rename $playlist to $name: $e") logE(e.stackTraceToString()) synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl } - return + false } } - override suspend fun deletePlaylist(playlist: Playlist) { - val playlistImpl = synchronized(this) { - requireNotNull(playlistMap[playlist.uid]) { "Cannot remove invalid playlist" }.also { - playlistMap.remove(it.uid) + override suspend fun deletePlaylist(playlist: Playlist): Boolean { + val playlistImpl = + synchronized(this) { + requireNotNull(playlistMap[playlist.uid]) { "Cannot remove invalid playlist" } + .also { playlistMap.remove(it.uid) } } - } - try { + + return try { playlistDao.deletePlaylist(playlist.uid) logD("Successfully deleted $playlist") + true } catch (e: Exception) { logE("Unable to delete $playlist: $e") logE(e.stackTraceToString()) synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl } - return + false } } - override suspend fun addToPlaylist(playlist: Playlist, songs: List) { - val playlistImpl = synchronized(this) { - requireNotNull(playlistMap[playlist.uid]) { "Cannot add to invalid playlist" }.also { - playlistMap[it.uid] = it.edit { addAll(songs) } + override suspend fun addToPlaylist(playlist: Playlist, songs: List): Boolean { + val playlistImpl = + synchronized(this) { + requireNotNull(playlistMap[playlist.uid]) { "Cannot add to invalid playlist" } + .also { playlistMap[it.uid] = it.edit { addAll(songs) } } } - } - try { + return try { playlistDao.insertPlaylistSongs(playlist.uid, songs.map { PlaylistSong(it.uid) }) logD("Successfully added ${songs.size} songs to $playlist") + true } catch (e: Exception) { logE("Unable to add ${songs.size} songs to $playlist: $e") logE(e.stackTraceToString()) synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl } - return + false } } - override suspend fun rewritePlaylist(playlist: Playlist, songs: List) { - val playlistImpl = synchronized(this) { - requireNotNull(playlistMap[playlist.uid]) { "Cannot rewrite invalid playlist" }.also { - playlistMap[it.uid] = it.edit(songs) + override suspend fun rewritePlaylist(playlist: Playlist, songs: List): Boolean { + val playlistImpl = + synchronized(this) { + requireNotNull(playlistMap[playlist.uid]) { "Cannot rewrite invalid playlist" } + .also { playlistMap[it.uid] = it.edit(songs) } } - } - try { + return try { playlistDao.replacePlaylistSongs(playlist.uid, songs.map { PlaylistSong(it.uid) }) logD("Successfully rewrote $playlist with ${songs.size} songs") + true } catch (e: Exception) { logE("Unable to rewrite $playlist with ${songs.size} songs: $e") logE(e.stackTraceToString()) synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl } - return + false } } } From bce03a5833262fbc5bd2e67f01cb6b4cfdc4ece9 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 8 Jun 2023 09:47:47 -0600 Subject: [PATCH 10/26] playback: do not dynamically extract adjustments Do not extract ReplayGain adjustments on the fly, instead doing them as we go along. This prevents an issue where the ReplayGain information would only be applied a short period after playback start, which is heavily jarring. --- .../java/org/oxycblt/auxio/music/Music.kt | 3 + .../auxio/music/cache/CacheDatabase.kt | 22 ++- .../oxycblt/auxio/music/cache/CacheModule.kt | 1 - .../auxio/music/device/DeviceMusicImpl.kt | 14 ++ .../oxycblt/auxio/music/device/RawMusic.kt | 4 + .../oxycblt/auxio/music/metadata/TagWorker.kt | 35 +++++ .../auxio/playback/replaygain/ReplayGain.kt | 8 + .../replaygain/ReplayGainAudioProcessor.kt | 141 ++++-------------- .../auxio/playback/system/PlaybackService.kt | 3 +- .../java/org/oxycblt/auxio/util/LangUtil.kt | 7 + 10 files changed, 113 insertions(+), 125 deletions(-) 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 a9783cfae..edb5a69e1 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -34,6 +34,7 @@ import org.oxycblt.auxio.music.info.Date import org.oxycblt.auxio.music.info.Disc import org.oxycblt.auxio.music.info.Name import org.oxycblt.auxio.music.info.ReleaseType +import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment import org.oxycblt.auxio.util.concatLocalized import org.oxycblt.auxio.util.toUuidOrNull @@ -255,6 +256,8 @@ interface Song : Music { val size: Long /** The duration of the audio file, in milliseconds. */ val durationMs: Long + /** The ReplayGain adjustment to apply during playback. */ + val replayGainAdjustment: ReplayGainAdjustment? /** The date the audio file was added to the device, as a unix epoch timestamp. */ val dateAdded: Long /** diff --git a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheDatabase.kt b/app/src/main/java/org/oxycblt/auxio/music/cache/CacheDatabase.kt index 2cf6d33c0..7d1ac68d1 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheDatabase.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/cache/CacheDatabase.kt @@ -32,19 +32,19 @@ import org.oxycblt.auxio.music.info.Date import org.oxycblt.auxio.music.metadata.correctWhitespace import org.oxycblt.auxio.music.metadata.splitEscaped -@Database(entities = [CachedSong::class], version = 27, exportSchema = false) +@Database(entities = [CachedSong::class], version = 32, exportSchema = false) abstract class CacheDatabase : RoomDatabase() { abstract fun cachedSongsDao(): CachedSongsDao } @Dao interface CachedSongsDao { - @Query("SELECT * FROM ${CachedSong.TABLE_NAME}") suspend fun readSongs(): List - @Query("DELETE FROM ${CachedSong.TABLE_NAME}") suspend fun nukeSongs() + @Query("SELECT * FROM CachedSong") suspend fun readSongs(): List + @Query("DELETE FROM CachedSong") suspend fun nukeSongs() @Insert suspend fun insertSongs(songs: List) } -@Entity(tableName = CachedSong.TABLE_NAME) +@Entity @TypeConverters(CachedSong.Converters::class) data class CachedSong( /** @@ -60,6 +60,10 @@ data class CachedSong( var size: Long? = null, /** @see RawSong */ var durationMs: Long, + /** @see RawSong.replayGainTrackAdjustment */ + val replayGainTrackAdjustment: Float?, + /** @see RawSong.replayGainAlbumAdjustment */ + val replayGainAlbumAdjustment: Float?, /** @see RawSong.musicBrainzId */ var musicBrainzId: String? = null, /** @see RawSong.name */ @@ -97,7 +101,7 @@ data class CachedSong( /** @see RawSong.genreNames */ var genreNames: List = listOf() ) { - fun copyToRaw(rawSong: RawSong): CachedSong { + fun copyToRaw(rawSong: RawSong) { rawSong.musicBrainzId = musicBrainzId rawSong.name = name rawSong.sortName = sortName @@ -105,6 +109,9 @@ data class CachedSong( rawSong.size = size rawSong.durationMs = durationMs + rawSong.replayGainTrackAdjustment = replayGainTrackAdjustment + rawSong.replayGainAlbumAdjustment = replayGainAlbumAdjustment + rawSong.track = track rawSong.disc = disc rawSong.subtitle = subtitle @@ -124,7 +131,6 @@ data class CachedSong( rawSong.albumArtistSortNames = albumArtistSortNames rawSong.genreNames = genreNames - return this } object Converters { @@ -141,8 +147,6 @@ data class CachedSong( } companion object { - const val TABLE_NAME = "cached_songs" - fun fromRaw(rawSong: RawSong) = CachedSong( mediaStoreId = @@ -155,6 +159,8 @@ data class CachedSong( sortName = rawSong.sortName, size = rawSong.size, durationMs = requireNotNull(rawSong.durationMs) { "Invalid raw: No duration" }, + replayGainTrackAdjustment = rawSong.replayGainTrackAdjustment, + replayGainAlbumAdjustment = rawSong.replayGainAlbumAdjustment, track = rawSong.track, disc = rawSong.disc, subtitle = rawSong.subtitle, diff --git a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheModule.kt b/app/src/main/java/org/oxycblt/auxio/music/cache/CacheModule.kt index 82e70f217..281cb6f4a 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/cache/CacheModule.kt @@ -43,7 +43,6 @@ class CacheRoomModule { Room.databaseBuilder( context.applicationContext, CacheDatabase::class.java, "music_cache.db") .fallbackToDestructiveMigration() - .fallbackToDestructiveMigrationFrom(0) .fallbackToDestructiveMigrationOnDowngrade() .build() diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt index e3ce99928..022d68c86 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt @@ -37,6 +37,8 @@ import org.oxycblt.auxio.music.info.Name import org.oxycblt.auxio.music.info.ReleaseType import org.oxycblt.auxio.music.metadata.parseId3GenreNames import org.oxycblt.auxio.music.metadata.parseMultiValue +import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment +import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.nonZeroOrNull import org.oxycblt.auxio.util.toUuidOrNull import org.oxycblt.auxio.util.unlikelyToBeNull @@ -88,6 +90,18 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son fromFormat = null) override val size = requireNotNull(rawSong.size) { "Invalid raw: No size" } override val durationMs = requireNotNull(rawSong.durationMs) { "Invalid raw: No duration" } + override val replayGainAdjustment = + if (rawSong.replayGainTrackAdjustment != null && + rawSong.replayGainAlbumAdjustment != null) { + ReplayGainAdjustment( + track = unlikelyToBeNull(rawSong.replayGainTrackAdjustment), + album = unlikelyToBeNull(rawSong.replayGainAlbumAdjustment)) + } else { + null + } + .also { + logD("${rawSong.replayGainTrackAdjustment} ${rawSong.replayGainAlbumAdjustment}}") + } override val dateAdded = requireNotNull(rawSong.dateAdded) { "Invalid raw: No date added" } private var _album: AlbumImpl? = null override val album: Album diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt b/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt index 46c84fc51..57539ec09 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt @@ -51,6 +51,10 @@ data class RawSong( var durationMs: Long? = null, /** @see Song.mimeType */ var extensionMimeType: String? = null, + /** @see Song.replayGainAdjustment */ + var replayGainTrackAdjustment: Float? = null, + /** @see Song.replayGainAdjustment */ + var replayGainAlbumAdjustment: Float? = null, /** @see Music.UID */ var musicBrainzId: String? = null, /** @see Music.name */ diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt index e1501bede..691f8997a 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt @@ -174,6 +174,13 @@ private class TagWorkerImpl( rawSong.albumArtistNames.ifEmpty { COMPILATION_ALBUM_ARTISTS } rawSong.releaseTypes = rawSong.releaseTypes.ifEmpty { COMPILATION_RELEASE_TYPES } } + // ReplayGain information + textFrames["TXXX:replaygain_track_gain"]?.parseReplayGainAdjustment()?.let { + rawSong.replayGainTrackAdjustment = it + } + textFrames["TXXX:replaygain_album_gain"]?.parseReplayGainAdjustment()?.let { + rawSong.replayGainAlbumAdjustment = it + } } private fun parseId3v23Date(textFrames: Map>): Date? { @@ -271,10 +278,38 @@ private class TagWorkerImpl( rawSong.albumArtistNames.ifEmpty { COMPILATION_ALBUM_ARTISTS } rawSong.releaseTypes = rawSong.releaseTypes.ifEmpty { COMPILATION_RELEASE_TYPES } } + + // ReplayGain information + // Most ReplayGain tags are formatted as a simple decibel adjustment in a custom + // replaygain_*_gain tag, but opus has it's own "r128_*_gain" ReplayGain specification, + // which requires dividing the adjustment by 256 to get the gain. This is used alongside + // the base adjustment intrinsic to the format to create the normalized adjustment. This is + // normally the only tag used for opus files, but some software still writes replay gain + // tags anyway. + (comments["r128_track_gain"]?.parseReplayGainAdjustment()?.div(256) + ?: comments["replaygain_track_gain"]?.parseReplayGainAdjustment()) + ?.let { rawSong.replayGainTrackAdjustment = it } + (comments["r128_album_gain"]?.parseReplayGainAdjustment()?.div(256) + ?: comments["replaygain_album_gain"]?.parseReplayGainAdjustment()) + ?.let { rawSong.replayGainAlbumAdjustment = it } } + /** + * Parse a ReplayGain adjustment into a float value. + * + * @return A parsed adjustment float, or null if the adjustment had invalid formatting. + */ + private fun List.parseReplayGainAdjustment() = + first().replace(REPLAYGAIN_ADJUSTMENT_FILTER_REGEX, "").toFloatOrNull() + private companion object { val COMPILATION_ALBUM_ARTISTS = listOf("Various Artists") val COMPILATION_RELEASE_TYPES = listOf("compilation") + + /** + * Matches non-float information from ReplayGain adjustments. Derived from vanilla music: + * https://github.com/vanilla-music/vanilla + */ + val REPLAYGAIN_ADJUSTMENT_FILTER_REGEX by lazy { Regex("[^\\d.-]") } } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGain.kt b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGain.kt index 0b1855a50..8a85b73e2 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGain.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGain.kt @@ -50,6 +50,14 @@ enum class ReplayGainMode { } } +/** + * Represents a ReplayGain adjustment to apply during song playback. + * + * @param track The track-specific adjustment that should be applied. + * @param album A more general album-specific adjustment that should be applied. + */ +data class ReplayGainAdjustment(val track: Float, val album: Float) + /** * The current ReplayGain pre-amp configuration. * diff --git a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt index ab86651e0..d74e45543 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt @@ -21,15 +21,16 @@ package org.oxycblt.auxio.playback.replaygain import androidx.media3.common.C import androidx.media3.common.Format import androidx.media3.common.Player -import androidx.media3.common.Tracks import androidx.media3.common.audio.AudioProcessor import androidx.media3.exoplayer.audio.BaseAudioProcessor import java.nio.ByteBuffer import javax.inject.Inject import kotlin.math.pow import org.oxycblt.auxio.music.Album -import org.oxycblt.auxio.music.metadata.TextTags +import org.oxycblt.auxio.music.MusicParent +import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.PlaybackSettings +import org.oxycblt.auxio.playback.queue.Queue import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.util.logD @@ -48,9 +49,7 @@ class ReplayGainAudioProcessor constructor( private val playbackManager: PlaybackStateManager, private val playbackSettings: PlaybackSettings -) : BaseAudioProcessor(), Player.Listener, PlaybackSettings.Listener { - private var lastFormat: Format? = null - +) : BaseAudioProcessor(), PlaybackStateManager.Listener, PlaybackSettings.Listener { private var volume = 1f set(value) { field = value @@ -58,51 +57,38 @@ constructor( flush() } - /** - * Add this instance to the components required for it to function correctly. - * - * @param player The [Player] to attach to. Should already have this instance as an audio - * processor. - */ - fun addToListeners(player: Player) { - player.addListener(this) + init { + playbackManager.addListener(this) playbackSettings.registerListener(this) } - /** - * Remove this instance from the components required for it to function correctly. - * - * @param player The [Player] to detach from. Should already have this instance as an audio - * processor. - */ - fun releaseFromListeners(player: Player) { - player.removeListener(this) + /** Remove this instance from the components required for it to function correctly. */ + fun release() { + playbackManager.removeListener(this) playbackSettings.unregisterListener(this) } // --- OVERRIDES --- - override fun onTracksChanged(tracks: Tracks) { - super.onTracksChanged(tracks) - // Try to find the currently playing track so we can update the ReplayGain adjustment - // based on it. - for (group in tracks.groups) { - if (group.isSelected) { - for (i in 0 until group.length) { - if (group.isTrackSelected(i)) { - applyReplayGain(group.getTrackFormat(i)) - return - } - } - } + override fun onIndexMoved(queue: Queue) { + logD("Index moved, updating current song") + applyReplayGain(queue.currentSong) + } + + override fun onQueueChanged(queue: Queue, change: Queue.Change) { + // Other types of queue changes preserve the current song. + if (change.type == Queue.Change.Type.SONG) { + applyReplayGain(queue.currentSong) } - // Nothing selected, apply nothing - applyReplayGain(null) + } + override fun onNewPlayback(queue: Queue, parent: MusicParent?) { + logD("New playback started, updating playback information") + applyReplayGain(queue.currentSong) } override fun onReplayGainSettingsChanged() { // ReplayGain config changed, we need to set it up again. - applyReplayGain(lastFormat) + applyReplayGain(playbackManager.queue.currentSong) } // --- REPLAYGAIN PARSING --- @@ -110,11 +96,11 @@ constructor( /** * Updates the volume adjustment based on the given [Format]. * - * @param format The [Format] of the currently playing track, or null if nothing is playing. + * @param song The [Format] of the currently playing track, or null if nothing is playing. */ - private fun applyReplayGain(format: Format?) { - lastFormat = format - val gain = parseReplayGain(format ?: return) + private fun applyReplayGain(song: Song?) { + logD("Applying ReplayGain adjustment for $song") + val gain = song?.replayGainAdjustment val preAmp = playbackSettings.replayGainPreAmp val adjust = @@ -167,58 +153,6 @@ constructor( volume = 10f.pow(adjust / 20f) } - /** - * Parse ReplayGain information from the given [Format]. - * - * @param format The [Format] to parse. - * @return A [Adjustment] adjustment, or null if there were no valid adjustments. - */ - private fun parseReplayGain(format: Format): Adjustment? { - val textTags = TextTags(format.metadata ?: return null) - var trackGain = 0f - var albumGain = 0f - - // Most ReplayGain tags are formatted as a simple decibel adjustment in a custom - // replaygain_*_gain tag. - textTags.id3v2["TXXX:$TAG_RG_TRACK_GAIN"] - ?.run { first().parseReplayGainAdjustment() } - ?.let { trackGain = it } - textTags.id3v2["TXXX:$TAG_RG_ALBUM_GAIN"] - ?.run { first().parseReplayGainAdjustment() } - ?.let { albumGain = it } - textTags.vorbis[TAG_RG_ALBUM_GAIN] - ?.run { first().parseReplayGainAdjustment() } - ?.let { trackGain = it } - textTags.vorbis[TAG_RG_TRACK_GAIN] - ?.run { first().parseReplayGainAdjustment() } - ?.let { albumGain = it } - - // Opus has it's own "r128_*_gain" ReplayGain specification, which requires dividing the - // adjustment by 256 to get the gain. This is used alongside the base adjustment - // intrinsic to the format to create the normalized adjustment. This is normally the only - // tag used for opus files, but some software still writes replay gain tags anyway. - textTags.vorbis[TAG_R128_TRACK_GAIN] - ?.run { first().parseReplayGainAdjustment() } - ?.let { trackGain = it / 256f } - textTags.vorbis[TAG_R128_ALBUM_GAIN] - ?.run { first().parseReplayGainAdjustment() } - ?.let { albumGain = it / 256f } - - return if (trackGain != 0f || albumGain != 0f) { - Adjustment(trackGain, albumGain) - } else { - null - } - } - - /** - * Parse a ReplayGain adjustment into a float value. - * - * @return A parsed adjustment float, or null if the adjustment had invalid formatting. - */ - private fun String.parseReplayGainAdjustment() = - replace(REPLAYGAIN_ADJUSTMENT_FILTER_REGEX, "").toFloatOrNull() - // --- AUDIO PROCESSOR IMPLEMENTATION --- override fun onConfigure( @@ -284,25 +218,4 @@ constructor( put(short.toByte()) put(short.toInt().shr(8).toByte()) } - - /** - * The resolved ReplayGain adjustment for a file. - * - * @param track The track adjustment (in dB), or 0 if it is not present. - * @param album The album adjustment (in dB), or 0 if it is not present. - */ - private data class Adjustment(val track: Float, val album: Float) - - private companion object { - const val TAG_RG_TRACK_GAIN = "replaygain_track_gain" - const val TAG_RG_ALBUM_GAIN = "replaygain_album_gain" - const val TAG_R128_TRACK_GAIN = "r128_track_gain" - const val TAG_R128_ALBUM_GAIN = "r128_album_gain" - - /** - * Matches non-float information from ReplayGain adjustments. Derived from vanilla music: - * https://github.com/vanilla-music/vanilla - */ - val REPLAYGAIN_ADJUSTMENT_FILTER_REGEX by lazy { Regex("[^\\d.-]") } - } } 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 848d47b4d..26629030b 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 @@ -144,7 +144,6 @@ class PlaybackService : true) .build() .also { it.addListener(this) } - replayGainProcessor.addToListeners(player) foregroundManager = ForegroundManager(this) // Initialize any listener-dependent components last as we wouldn't want a listener race // condition to cause us to load music before we were fully initialize. @@ -196,7 +195,7 @@ class PlaybackService : widgetComponent.release() mediaSessionComponent.release() - replayGainProcessor.releaseFromListeners(player) + replayGainProcessor.release() player.release() if (openAudioEffectSession) { // Make sure to close the audio session when we release the player. diff --git a/app/src/main/java/org/oxycblt/auxio/util/LangUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/LangUtil.kt index 51835d68c..3ad2f8eb1 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/LangUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/LangUtil.kt @@ -50,6 +50,13 @@ fun Int.nonZeroOrNull() = if (this > 0) this else null */ fun Long.nonZeroOrNull() = if (this > 0) this else null +/** + * Aliases a check to ensure that the given number is non-zero. + * + * @return The same number if it's non-zero, null otherwise. + */ +fun Float.nonZeroOrNull() = if (this > 0) this else null + /** * Aliases a check to ensure a given value is in a specified range. * From aaba858fcf5a9cf7e0d29b7c1dfc776934ecdc71 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 8 Jun 2023 10:15:04 -0600 Subject: [PATCH 11/26] all: only use one fallback method for dbs Only use fallbackToDestructiveMigration for all databases. I thought you had to selectively enable downgrading as well, but apparently that disables other destructive migrations. This was not obvious at all and caused countless issues. Absolutely flooring. --- app/src/main/java/org/oxycblt/auxio/music/cache/CacheModule.kt | 1 - app/src/main/java/org/oxycblt/auxio/music/user/UserModule.kt | 2 -- .../org/oxycblt/auxio/playback/persist/PersistenceModule.kt | 2 -- 3 files changed, 5 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheModule.kt b/app/src/main/java/org/oxycblt/auxio/music/cache/CacheModule.kt index 281cb6f4a..16de62c99 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/cache/CacheModule.kt @@ -43,7 +43,6 @@ class CacheRoomModule { Room.databaseBuilder( context.applicationContext, CacheDatabase::class.java, "music_cache.db") .fallbackToDestructiveMigration() - .fallbackToDestructiveMigrationOnDowngrade() .build() @Provides fun cachedSongsDao(database: CacheDatabase) = database.cachedSongsDao() diff --git a/app/src/main/java/org/oxycblt/auxio/music/user/UserModule.kt b/app/src/main/java/org/oxycblt/auxio/music/user/UserModule.kt index 10e55c5bd..10a42edb1 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/user/UserModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/user/UserModule.kt @@ -43,7 +43,5 @@ class UserRoomModule { Room.databaseBuilder( context.applicationContext, UserMusicDatabase::class.java, "user_music.db") .fallbackToDestructiveMigration() - .fallbackToDestructiveMigrationFrom(0) - .fallbackToDestructiveMigrationOnDowngrade() .build() } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceModule.kt b/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceModule.kt index 76af5369b..73ad12e07 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceModule.kt @@ -45,8 +45,6 @@ class PersistenceRoomModule { PersistenceDatabase::class.java, "playback_persistence.db") .fallbackToDestructiveMigration() - .fallbackToDestructiveMigrationFrom(1) - .fallbackToDestructiveMigrationOnDowngrade() .build() @Provides fun playbackStateDao(database: PersistenceDatabase) = database.playbackStateDao() From 59280cc6d58c88efa0f70b2d76f97908933b0ab5 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 8 Jun 2023 10:23:07 -0600 Subject: [PATCH 12/26] playback: standardize table names Just let room pick the table names for PersistenceDatabase, instead of using custom ones. Reduces the amount of code in that part of the app. --- .../playback/persist/PersistenceDatabase.kt | 54 ++++++++----------- .../playback/persist/PersistenceModule.kt | 1 + 2 files changed, 24 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceDatabase.kt b/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceDatabase.kt index 93e387068..466b56812 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceDatabase.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceDatabase.kt @@ -27,6 +27,7 @@ import androidx.room.PrimaryKey import androidx.room.Query import androidx.room.RoomDatabase import androidx.room.TypeConverters +import androidx.room.migration.Migration import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.playback.state.RepeatMode @@ -37,7 +38,7 @@ import org.oxycblt.auxio.playback.state.RepeatMode */ @Database( entities = [PlaybackState::class, QueueHeapItem::class, QueueMappingItem::class], - version = 27, + version = 32, exportSchema = false) @TypeConverters(Music.UID.TypeConverters::class) abstract class PersistenceDatabase : RoomDatabase() { @@ -54,6 +55,16 @@ abstract class PersistenceDatabase : RoomDatabase() { * @return A [QueueDao] providing control of the database's queue tables. */ abstract fun queueDao(): QueueDao + + companion object { + val MIGRATION_27_32 = + Migration(27, 32) { + // Switched from custom names to just letting room pick the names + it.execSQL("ALTER TABLE playback_state RENAME TO PlaybackState") + it.execSQL("ALTER TABLE queue_heap RENAME TO QueueHeapItem") + it.execSQL("ALTER TABLE queue_mapping RENAME TO QueueMappingItem") + } + } } /** @@ -68,11 +79,10 @@ interface PlaybackStateDao { * * @return The previously persisted [PlaybackState], or null if one was not present. */ - @Query("SELECT * FROM ${PlaybackState.TABLE_NAME} WHERE id = 0") - suspend fun getState(): PlaybackState? + @Query("SELECT * FROM PlaybackState WHERE id = 0") suspend fun getState(): PlaybackState? /** Delete any previously persisted [PlaybackState]s. */ - @Query("DELETE FROM ${PlaybackState.TABLE_NAME}") suspend fun nukeState() + @Query("DELETE FROM PlaybackState") suspend fun nukeState() /** * Insert a new [PlaybackState] into the database. @@ -94,21 +104,20 @@ interface QueueDao { * * @return A list of persisted [QueueHeapItem]s wrapping each heap item. */ - @Query("SELECT * FROM ${QueueHeapItem.TABLE_NAME}") suspend fun getHeap(): List + @Query("SELECT * FROM QueueHeapItem") suspend fun getHeap(): List /** * Get the previously persisted queue mapping. * * @return A list of persisted [QueueMappingItem]s wrapping each heap item. */ - @Query("SELECT * FROM ${QueueMappingItem.TABLE_NAME}") - suspend fun getMapping(): List + @Query("SELECT * FROM QueueMappingItem") suspend fun getMapping(): List /** Delete any previously persisted queue heap entries. */ - @Query("DELETE FROM ${QueueHeapItem.TABLE_NAME}") suspend fun nukeHeap() + @Query("DELETE FROM QueueHeapItem") suspend fun nukeHeap() /** Delete any previously persisted queue mapping entries. */ - @Query("DELETE FROM ${QueueMappingItem.TABLE_NAME}") suspend fun nukeMapping() + @Query("DELETE FROM QueueMappingItem") suspend fun nukeMapping() /** * Insert new heap entries into the database. @@ -128,7 +137,7 @@ interface QueueDao { // TODO: Figure out how to get RepeatMode to map to an int instead of a string // TODO: Use intrinsic table names rather than custom names -@Entity(tableName = PlaybackState.TABLE_NAME) +@Entity data class PlaybackState( @PrimaryKey val id: Int, val index: Int, @@ -136,26 +145,9 @@ data class PlaybackState( val repeatMode: RepeatMode, val songUid: Music.UID, val parentUid: Music.UID? -) { - companion object { - const val TABLE_NAME = "playback_state" - } -} +) -@Entity(tableName = QueueHeapItem.TABLE_NAME) -data class QueueHeapItem(@PrimaryKey val id: Int, val uid: Music.UID) { - companion object { - const val TABLE_NAME = "queue_heap" - } -} +@Entity data class QueueHeapItem(@PrimaryKey val id: Int, val uid: Music.UID) -@Entity(tableName = QueueMappingItem.TABLE_NAME) -data class QueueMappingItem( - @PrimaryKey val id: Int, - val orderedIndex: Int, - val shuffledIndex: Int -) { - companion object { - const val TABLE_NAME = "queue_mapping" - } -} +@Entity +data class QueueMappingItem(@PrimaryKey val id: Int, val orderedIndex: Int, val shuffledIndex: Int) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceModule.kt b/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceModule.kt index 73ad12e07..fd82e75d4 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceModule.kt @@ -45,6 +45,7 @@ class PersistenceRoomModule { PersistenceDatabase::class.java, "playback_persistence.db") .fallbackToDestructiveMigration() + .addMigrations(PersistenceDatabase.MIGRATION_27_32) .build() @Provides fun playbackStateDao(database: PersistenceDatabase) = database.playbackStateDao() From abe4255986f2916c018ecbb9db6e3d6153136cb1 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 8 Jun 2023 13:33:30 -0600 Subject: [PATCH 13/26] list: actually update playlist selections Remove an incorrect change check resulting in the selection not updating when playlist information changed. Resolves #476. --- CHANGELOG.md | 1 + .../java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72e2fbf10..4c61131ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ #### What's Fixed - Disc number is no longer mis-aligned when no subtitle is present +- Fixed selection not updating when playlists are changed ## 3.1.1 diff --git a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt index a5cfd776e..3f4efdd0e 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt @@ -56,7 +56,6 @@ constructor( } override fun onMusicChanges(changes: MusicRepository.Changes) { - if (!changes.deviceLibrary) return val deviceLibrary = musicRepository.deviceLibrary ?: return val userLibrary = musicRepository.userLibrary ?: return // Sanitize the selection to remove items that no longer exist and thus From 93d3e821401b06af85feb7dd96a1b65214594949 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 8 Jun 2023 13:44:04 -0600 Subject: [PATCH 14/26] ui: use targetstate more Use targetState whenever making decisions on whether a certain sheet state change is valid. This is mostly for stylistic consistency and has no effect on UX. --- app/src/main/java/org/oxycblt/auxio/MainFragment.kt | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index ba09eddf0..ecc8f21f5 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -149,7 +149,7 @@ class MainFragment : logD("Configuring stacked bottom sheets") unlikelyToBeNull(binding.queueHandleWrapper).setOnClickListener { if (playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED && - queueSheetBehavior.state == BackportBottomSheetBehavior.STATE_COLLAPSED) { + queueSheetBehavior.targetState == BackportBottomSheetBehavior.STATE_COLLAPSED) { // Playback sheet is expanded and queue sheet is collapsed, we can expand it. queueSheetBehavior.state = BackportBottomSheetBehavior.STATE_EXPANDED } @@ -414,7 +414,7 @@ class MainFragment : val playbackSheetBehavior = binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior - if (playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_COLLAPSED) { + if (playbackSheetBehavior.targetState == BackportBottomSheetBehavior.STATE_COLLAPSED) { // Playback sheet is not expanded and not hidden, we can expand it. logD("Expanding playback sheet") playbackSheetBehavior.state = BackportBottomSheetBehavior.STATE_EXPANDED @@ -424,9 +424,9 @@ class MainFragment : val queueSheetBehavior = (binding.queueSheet.coordinatorLayoutBehavior ?: return) as QueueBottomSheetBehavior if (playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED && - queueSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED) { + queueSheetBehavior.targetState == BackportBottomSheetBehavior.STATE_EXPANDED) { // Queue sheet and playback sheet is expanded, close the queue sheet so the - // playback panel can eb shown. + // playback panel can shown. logD("Collapsing queue sheet") queueSheetBehavior.state = BackportBottomSheetBehavior.STATE_COLLAPSED } @@ -436,7 +436,7 @@ class MainFragment : val binding = requireBinding() val playbackSheetBehavior = binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior - if (playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED) { + if (playbackSheetBehavior.targetState == BackportBottomSheetBehavior.STATE_EXPANDED) { // Playback sheet (and possibly queue) needs to be collapsed. logD("Collapsing playback and queue sheets") val queueSheetBehavior = @@ -487,8 +487,6 @@ class MainFragment : } } - // TODO: Use targetState more - private class SheetBackPressedCallback( private val playbackSheetBehavior: PlaybackBottomSheetBehavior<*>, private val queueSheetBehavior: QueueBottomSheetBehavior<*>? From e848bea0bfdfd564da831127430095384340190e Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 8 Jun 2023 14:09:09 -0600 Subject: [PATCH 15/26] music: remove zero digit-likes from names Remove all characters with a semantic meaning of "zero" from the start of music names, at least when intelligent sorting is enabled. --- app/src/main/java/org/oxycblt/auxio/music/info/Name.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/music/info/Name.kt b/app/src/main/java/org/oxycblt/auxio/music/info/Name.kt index 838f8f5d5..4a8c0d3d9 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/info/Name.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/info/Name.kt @@ -203,8 +203,7 @@ private data class IntelligentKnownName(override val raw: String, override val s // Separate each token into their numeric and lexicographic counterparts. if (token.first().isDigit()) { // The digit string comparison breaks with preceding zero digits, remove those - // TODO: Handle zero digits in other languages - val digits = token.trimStart('0').ifEmpty { token } + val digits = token.trimStart { Character.getNumericValue(it) == 0 }.ifEmpty { token } // Other languages have other types of digit strings, still use collation keys collationKey = COLLATOR.getCollationKey(digits) type = SortToken.Type.NUMERIC From 672c256b1e738914793c0969f56ee3af5957f4d9 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 8 Jun 2023 20:40:09 -0600 Subject: [PATCH 16/26] music: group albums by raw artist keys Properly group albums by raw artist keys, instead of by raw artist instances. This was an accidental regression in 3.1.1 that resulted in duplicate albums in some circumstances. Resolves #475. --- .../auxio/music/device/DeviceMusicImpl.kt | 18 +++++++----------- .../org/oxycblt/auxio/music/device/RawMusic.kt | 14 ++++++++++---- .../java/org/oxycblt/auxio/music/info/Name.kt | 3 ++- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt index 022d68c86..aaad318b3 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt @@ -38,7 +38,6 @@ import org.oxycblt.auxio.music.info.ReleaseType import org.oxycblt.auxio.music.metadata.parseId3GenreNames import org.oxycblt.auxio.music.metadata.parseMultiValue import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment -import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.nonZeroOrNull import org.oxycblt.auxio.util.toUuidOrNull import org.oxycblt.auxio.util.unlikelyToBeNull @@ -92,16 +91,13 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son override val durationMs = requireNotNull(rawSong.durationMs) { "Invalid raw: No duration" } override val replayGainAdjustment = if (rawSong.replayGainTrackAdjustment != null && - rawSong.replayGainAlbumAdjustment != null) { - ReplayGainAdjustment( - track = unlikelyToBeNull(rawSong.replayGainTrackAdjustment), - album = unlikelyToBeNull(rawSong.replayGainAlbumAdjustment)) - } else { - null - } - .also { - logD("${rawSong.replayGainTrackAdjustment} ${rawSong.replayGainAlbumAdjustment}}") - } + rawSong.replayGainAlbumAdjustment != null) { + ReplayGainAdjustment( + track = unlikelyToBeNull(rawSong.replayGainTrackAdjustment), + album = unlikelyToBeNull(rawSong.replayGainAlbumAdjustment)) + } else { + null + } override val dateAdded = requireNotNull(rawSong.dateAdded) { "Invalid raw: No date added" } private var _album: AlbumImpl? = null override val album: Album diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt b/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt index 57539ec09..5358f0244 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt @@ -25,6 +25,7 @@ import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.fs.Directory import org.oxycblt.auxio.music.info.Date import org.oxycblt.auxio.music.info.ReleaseType +import org.oxycblt.auxio.util.logD /** * Raw information about a [SongImpl] obtained from the filesystem/Extractor instances. @@ -128,10 +129,12 @@ data class RawAlbum( // artist name. This allows for case-insensitive artist/album grouping, which can be common // for albums/artists that have different naming (ex. "RAMMSTEIN" vs. "Rammstein"). + private val artistKeys = inner.rawArtists.map { it.key } + // Cache the hash-code for HashMap efficiency. private val hashCode = inner.musicBrainzId?.hashCode() - ?: (31 * inner.name.lowercase().hashCode() + inner.rawArtists.hashCode()) + ?: (31 * inner.name.lowercase().hashCode() + artistKeys.hashCode()) override fun hashCode() = hashCode @@ -141,8 +144,7 @@ data class RawAlbum( inner.musicBrainzId != null && other.inner.musicBrainzId != null -> inner.musicBrainzId == other.inner.musicBrainzId inner.musicBrainzId == null && other.inner.musicBrainzId == null -> - inner.name.equals(other.inner.name, true) && - inner.rawArtists == other.inner.rawArtists + inner.name.equals(other.inner.name, true) && artistKeys == other.artistKeys else -> false } } @@ -176,7 +178,11 @@ data class RawArtist( // grouping to be case-insensitive. // Cache the hashCode for HashMap efficiency. - private val hashCode = inner.musicBrainzId?.hashCode() ?: inner.name?.lowercase().hashCode() + val hashCode = inner.musicBrainzId?.hashCode() ?: inner.name?.lowercase().hashCode() + + init { + logD("${inner.name} ${inner.name?.lowercase().hashCode()} $hashCode") + } // Compare names and MusicBrainz IDs in order to differentiate artists with the // same name in large libraries. diff --git a/app/src/main/java/org/oxycblt/auxio/music/info/Name.kt b/app/src/main/java/org/oxycblt/auxio/music/info/Name.kt index 4a8c0d3d9..03f3a33f6 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/info/Name.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/info/Name.kt @@ -203,7 +203,8 @@ private data class IntelligentKnownName(override val raw: String, override val s // Separate each token into their numeric and lexicographic counterparts. if (token.first().isDigit()) { // The digit string comparison breaks with preceding zero digits, remove those - val digits = token.trimStart { Character.getNumericValue(it) == 0 }.ifEmpty { token } + val digits = + token.trimStart { Character.getNumericValue(it) == 0 }.ifEmpty { token } // Other languages have other types of digit strings, still use collation keys collationKey = COLLATOR.getCollationKey(digits) type = SortToken.Type.NUMERIC From 4ee20bc112606161c7a86534a4d445dcf7ef6bab Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Fri, 9 Jun 2023 10:07:27 -0600 Subject: [PATCH 17/26] playback: fix regression regarding partial rgadj Fix a regression where partial ReplayGain adjustments missing an album or track component would not be indexed. This was an oversight made when moving adjustments to the cache, as the nullability model of ReplayGain adjustments changed. --- CHANGELOG.md | 2 + .../java/org/oxycblt/auxio/music/Music.kt | 2 +- .../auxio/music/device/DeviceLibrary.kt | 5 +- .../auxio/music/device/DeviceMusicImpl.kt | 11 +-- .../oxycblt/auxio/music/device/RawMusic.kt | 1 - .../oxycblt/auxio/music/metadata/TextTags.kt | 2 + .../playback/picker/PlayFromArtistDialog.kt | 2 +- .../playback/picker/PlayFromGenreDialog.kt | 2 +- .../auxio/playback/replaygain/ReplayGain.kt | 7 +- .../replaygain/ReplayGainAudioProcessor.kt | 82 +++++++++---------- 10 files changed, 57 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c61131ac..1f121f950 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,13 @@ #### What's Improved - Tags formatted as `artistssort` or `albumartistssort` are now recognized by Auxio +- Non-english digit strings are now sorted better - Reduced visual loading time #### What's Fixed - Disc number is no longer mis-aligned when no subtitle is present - Fixed selection not updating when playlists are changed +- Fixed duplicate albums appearing in certain cases ## 3.1.1 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 edb5a69e1..395b17d93 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -257,7 +257,7 @@ interface Song : Music { /** The duration of the audio file, in milliseconds. */ val durationMs: Long /** The ReplayGain adjustment to apply during playback. */ - val replayGainAdjustment: ReplayGainAdjustment? + val replayGainAdjustment: ReplayGainAdjustment /** The date the audio file was added to the device, as a unix epoch timestamp. */ val dateAdded: Long /** diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt index 08e312ed5..9ec43ee9b 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt @@ -215,9 +215,8 @@ class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: Mu is SongImpl -> body.raw = PrioritizedRaw(rawArtist, album) is AlbumImpl -> { // Album information from later dates is prioritized, as it is more - // likely to - // contain the "modern" name of the artist if the information really is - // in-consistent. Fall back to the name otherwise. + // likely to contain the "modern" name of the artist if the information + // really is in-consistent. Fall back to the name otherwise. val dateEarlier = album.dates != null && (prioritized.dates == null || album.dates < prioritized.dates) diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt index aaad318b3..82827b59b 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt @@ -90,14 +90,9 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son override val size = requireNotNull(rawSong.size) { "Invalid raw: No size" } override val durationMs = requireNotNull(rawSong.durationMs) { "Invalid raw: No duration" } override val replayGainAdjustment = - if (rawSong.replayGainTrackAdjustment != null && - rawSong.replayGainAlbumAdjustment != null) { - ReplayGainAdjustment( - track = unlikelyToBeNull(rawSong.replayGainTrackAdjustment), - album = unlikelyToBeNull(rawSong.replayGainAlbumAdjustment)) - } else { - null - } + ReplayGainAdjustment( + track = rawSong.replayGainTrackAdjustment, album = rawSong.replayGainAlbumAdjustment) + override val dateAdded = requireNotNull(rawSong.dateAdded) { "Invalid raw: No date added" } private var _album: AlbumImpl? = null override val album: Album diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt b/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt index 5358f0244..5828ac75a 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt @@ -128,7 +128,6 @@ data class RawAlbum( // - If we do not have a MusicBrainz ID, compare by the lowercase album name and lowercase // artist name. This allows for case-insensitive artist/album grouping, which can be common // for albums/artists that have different naming (ex. "RAMMSTEIN" vs. "Rammstein"). - private val artistKeys = inner.rawArtists.map { it.key } // Cache the hash-code for HashMap efficiency. diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/TextTags.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/TextTags.kt index cecd572ba..a3d916b69 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/TextTags.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/TextTags.kt @@ -28,6 +28,8 @@ import androidx.media3.extractor.metadata.vorbis.VorbisComment * * @param metadata The [Metadata] to wrap. * @author Alexander Capehart (OxygenCobalt) + * + * TODO: Merge with TagWorker */ class TextTags(metadata: Metadata) { private val _id3v2 = mutableMapOf>() diff --git a/app/src/main/java/org/oxycblt/auxio/playback/picker/PlayFromArtistDialog.kt b/app/src/main/java/org/oxycblt/auxio/playback/picker/PlayFromArtistDialog.kt index c8fc134e7..e42e6ff61 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/picker/PlayFromArtistDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/picker/PlayFromArtistDialog.kt @@ -81,7 +81,7 @@ class PlayFromArtistDialog : override fun onDestroyBinding(binding: DialogMusicChoicesBinding) { super.onDestroyBinding(binding) - choiceAdapter + binding.choiceRecycler.adapter = null } override fun onClick(item: Artist, viewHolder: RecyclerView.ViewHolder) { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/picker/PlayFromGenreDialog.kt b/app/src/main/java/org/oxycblt/auxio/playback/picker/PlayFromGenreDialog.kt index 1f2693a10..6811e3510 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/picker/PlayFromGenreDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/picker/PlayFromGenreDialog.kt @@ -81,7 +81,7 @@ class PlayFromGenreDialog : override fun onDestroyBinding(binding: DialogMusicChoicesBinding) { super.onDestroyBinding(binding) - choiceAdapter + binding.choiceRecycler.adapter = null } override fun onClick(item: Genre, viewHolder: RecyclerView.ViewHolder) { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGain.kt b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGain.kt index 8a85b73e2..1495fd6fb 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGain.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGain.kt @@ -53,10 +53,11 @@ enum class ReplayGainMode { /** * Represents a ReplayGain adjustment to apply during song playback. * - * @param track The track-specific adjustment that should be applied. - * @param album A more general album-specific adjustment that should be applied. + * @param track The track-specific adjustment that should be applied. Null if not available. + * @param album A more general album-specific adjustment that should be applied. Null if not + * available. */ -data class ReplayGainAdjustment(val track: Float, val album: Float) +data class ReplayGainAdjustment(val track: Float?, val album: Float?) /** * The current ReplayGain pre-amp configuration. diff --git a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt index d74e45543..5c039ab8c 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt @@ -99,58 +99,58 @@ constructor( * @param song The [Format] of the currently playing track, or null if nothing is playing. */ private fun applyReplayGain(song: Song?) { + if (song == null) { + logD("Nothing playing, disabling adjustment") + volume = 1f + return + } + logD("Applying ReplayGain adjustment for $song") - val gain = song?.replayGainAdjustment + + val gain = song.replayGainAdjustment val preAmp = playbackSettings.replayGainPreAmp - val adjust = - if (gain != null) { - logD("Found ReplayGain adjustment $gain") - // ReplayGain is configurable, so determine what to do based off of the mode. - val useAlbumGain = - when (playbackSettings.replayGainMode) { - // User wants track gain to be preferred. Default to album gain only if - // there is no track gain. - ReplayGainMode.TRACK -> { - logD("Using track strategy") - gain.track == 0f - } - // User wants album gain to be preferred. Default to track gain only if - // here is no album gain. - ReplayGainMode.ALBUM -> { - logD("Using album strategy") - gain.album != 0f - } - // User wants album gain to be used when in an album, track gain otherwise. - ReplayGainMode.DYNAMIC -> { - logD("Using dynamic strategy") - playbackManager.parent is Album && - playbackManager.queue.currentSong?.album == playbackManager.parent - } + // ReplayGain is configurable, so determine what to do based off of the mode. + val resolvedAdjustment = + when (playbackSettings.replayGainMode) { + // User wants track gain to be preferred. Default to album gain only if + // there is no track gain. + ReplayGainMode.TRACK -> { + logD("Using track strategy") + gain.track ?: gain.album + } + // User wants album gain to be preferred. Default to track gain only if + // here is no album gain. + ReplayGainMode.ALBUM -> { + logD("Using album strategy") + gain.album ?: gain.track + } + // User wants album gain to be used when in an album, track gain otherwise. + ReplayGainMode.DYNAMIC -> { + logD("Using dynamic strategy") + gain.album?.takeIf { + playbackManager.parent is Album && + playbackManager.queue.currentSong?.album == playbackManager.parent } + ?: gain.track + } + } - val resolvedGain = - if (useAlbumGain) { - logD("Using album gain") - gain.album - } else { - logD("Using track gain") - gain.track - } - - // Apply the adjustment specified when there is ReplayGain tags. - resolvedGain + preAmp.with + val amplifiedAdjustment = + if (resolvedAdjustment != null) { + // Successfully resolved an adjustment, apply the corresponding pre-amp + logD("Applying with pre-amp") + resolvedAdjustment + preAmp.with } else { - // No ReplayGain tags existed, or no tags were parsable, or there was no metadata - // in the first place. Return the gain to use when there is no ReplayGain value. - logD("No ReplayGain tags present") + // No adjustment found, use the corresponding user-defined pre-amp + logD("Applying without pre-amp") preAmp.without } - logD("Applying ReplayGain adjustment ${adjust}db") + logD("Applying ReplayGain adjustment ${amplifiedAdjustment}db") // Final adjustment along the volume curve. - volume = 10f.pow(adjust / 20f) + volume = 10f.pow(amplifiedAdjustment / 20f) } // --- AUDIO PROCESSOR IMPLEMENTATION --- From 0771a75b5a64ccf6b90c83b346d1dc8d5122e3cb Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Tue, 13 Jun 2023 08:57:03 -0600 Subject: [PATCH 18/26] playback: ignore zeroed replaygain tags Ignore ReplayGain adjustments that are just 0 dB. --- CHANGELOG.md | 1 + .../main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f121f950..fa466a8bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Disc number is no longer mis-aligned when no subtitle is present - Fixed selection not updating when playlists are changed - Fixed duplicate albums appearing in certain cases +- Fixed ReplayGain adjustment not applying at the start of a song in certain cases ## 3.1.1 diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt index 691f8997a..69569fb19 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt @@ -30,6 +30,7 @@ import org.oxycblt.auxio.music.fs.toAudioUri import org.oxycblt.auxio.music.info.Date import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logW +import org.oxycblt.auxio.util.nonZeroOrNull /** * An processing abstraction over the [MetadataRetriever] and [TextTags] workflow that operates on @@ -300,7 +301,7 @@ private class TagWorkerImpl( * @return A parsed adjustment float, or null if the adjustment had invalid formatting. */ private fun List.parseReplayGainAdjustment() = - first().replace(REPLAYGAIN_ADJUSTMENT_FILTER_REGEX, "").toFloatOrNull() + first().replace(REPLAYGAIN_ADJUSTMENT_FILTER_REGEX, "").toFloatOrNull()?.nonZeroOrNull() private companion object { val COMPILATION_ALBUM_ARTISTS = listOf("Various Artists") From 530e427b79509a0ada03b03f52235e8432ce4e78 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Tue, 13 Jun 2023 08:58:08 -0600 Subject: [PATCH 19/26] music: improve library updates Update library information in a synchronized block and then only dispatch the update on the main thread. --- .../oxycblt/auxio/music/MusicRepository.kt | 46 +++++++++++-------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt index 6fa6801fb..513786f87 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -32,6 +32,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlinx.coroutines.yield import org.oxycblt.auxio.music.cache.CacheRepository import org.oxycblt.auxio.music.device.DeviceLibrary @@ -300,35 +301,35 @@ constructor( val userLibrary = synchronized(this) { userLibrary ?: return } logD("Creating playlist $name with ${songs.size} songs") userLibrary.createPlaylist(name, songs) - emitLibraryChange(device = false, user = true) + dispatchLibraryChange(device = false, user = true) } override suspend fun renamePlaylist(playlist: Playlist, name: String) { val userLibrary = synchronized(this) { userLibrary ?: return } logD("Renaming $playlist to $name") userLibrary.renamePlaylist(playlist, name) - emitLibraryChange(device = false, user = true) + dispatchLibraryChange(device = false, user = true) } override suspend fun deletePlaylist(playlist: Playlist) { val userLibrary = synchronized(this) { userLibrary ?: return } logD("Deleting $playlist") userLibrary.deletePlaylist(playlist) - emitLibraryChange(device = false, user = true) + dispatchLibraryChange(device = false, user = true) } override suspend fun addToPlaylist(songs: List, playlist: Playlist) { val userLibrary = synchronized(this) { userLibrary ?: return } logD("Adding ${songs.size} songs to $playlist") userLibrary.addToPlaylist(playlist, songs) - emitLibraryChange(device = false, user = true) + dispatchLibraryChange(device = false, user = true) } override suspend fun rewritePlaylist(playlist: Playlist, songs: List) { val userLibrary = synchronized(this) { userLibrary ?: return } logD("Rewriting $playlist with ${songs.size} songs") userLibrary.rewritePlaylist(playlist, songs) - emitLibraryChange(device = false, user = true) + dispatchLibraryChange(device = false, user = true) } @Synchronized @@ -353,7 +354,7 @@ constructor( // Music loading process failed due to something we have not handled. logE("Music indexing failed") logE(e.stackTraceToString()) - emitComplete(e) + emitIndexingCompletion(e) } } @@ -367,7 +368,7 @@ constructor( // Start initializing the extractors. Use an indeterminate state, as there is no ETA on // how long a media database query will take. - emitLoading(IndexingProgress.Indeterminate) + emitIndexingProgress(IndexingProgress.Indeterminate) // Do the initial query of the cache and media databases in parallel. logD("Starting MediaStore query") @@ -413,7 +414,7 @@ constructor( val rawSongs = LinkedList() for (rawSong in processedSongs) { rawSongs.add(rawSong) - emitLoading(IndexingProgress.Songs(rawSongs.size, query.projectedTotal)) + emitIndexingProgress(IndexingProgress.Songs(rawSongs.size, query.projectedTotal)) } logD("Awaiting discovery completion") // These should be no-ops, but we need the error state to see if we should keep going. @@ -428,7 +429,7 @@ constructor( // Successfully loaded the library, now save the cache and read playlist information // in parallel. logD("Discovered ${rawSongs.size} songs, starting finalization") - emitLoading(IndexingProgress.Indeterminate) + emitIndexingProgress(IndexingProgress.Indeterminate) logD("Starting UserLibrary query") val userLibraryQueryJob = worker.scope.tryAsync { userLibraryFactory.query() } if (cache == null || cache.invalidated) { @@ -443,22 +444,27 @@ constructor( val userLibrary = userLibraryFactory.create(rawPlaylists, deviceLibrary) logD("Successfully indexed music library [device=$deviceLibrary user=$userLibrary]") - emitComplete(null) + emitIndexingCompletion(null) // Comparing the library instances is obscenely expensive, do it within the library - val deviceLibraryChanged = this.deviceLibrary != deviceLibrary - val userLibraryChanged = this.userLibrary != userLibrary - if (!deviceLibraryChanged && !userLibraryChanged) { - logD("Library has not changed, skipping update") - return - } + val deviceLibraryChanged: Boolean + val userLibraryChanged: Boolean synchronized(this) { + deviceLibraryChanged = this.deviceLibrary != deviceLibrary + userLibraryChanged = this.userLibrary != userLibrary + if (!deviceLibraryChanged && !userLibraryChanged) { + logD("Library has not changed, skipping update") + return + } + this.deviceLibrary = deviceLibrary this.userLibrary = userLibrary } - emitLibraryChange(deviceLibraryChanged, userLibraryChanged) + withContext(Dispatchers.Main) { + dispatchLibraryChange(deviceLibraryChanged, userLibraryChanged) + } } /** @@ -477,7 +483,7 @@ constructor( } } - private suspend fun emitLoading(progress: IndexingProgress) { + private suspend fun emitIndexingProgress(progress: IndexingProgress) { yield() synchronized(this) { currentIndexingState = IndexingState.Indexing(progress) @@ -487,7 +493,7 @@ constructor( } } - private suspend fun emitComplete(error: Exception?) { + private suspend fun emitIndexingCompletion(error: Exception?) { yield() synchronized(this) { previousCompletedState = IndexingState.Completed(error) @@ -500,7 +506,7 @@ constructor( } @Synchronized - private fun emitLibraryChange(device: Boolean, user: Boolean) { + private fun dispatchLibraryChange(device: Boolean, user: Boolean) { val changes = MusicRepository.Changes(device, user) logD("Dispatching library change [changes=$changes]") for (listener in updateListeners) { From 5b44c31689bd6d1d825f59c822358cd4ed333893 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Tue, 13 Jun 2023 09:37:18 -0600 Subject: [PATCH 20/26] music: sort song information Sort song informtion in all MusicParent instances. This is a temporary hack to band-aid music consistency between reloads, as with the aggressive parallelization song order is no longer consistent. --- .../auxio/music/device/DeviceLibrary.kt | 14 ++--- .../auxio/music/device/DeviceMusicImpl.kt | 52 +++++++++---------- .../oxycblt/auxio/music/device/RawMusic.kt | 16 +++--- 3 files changed, 37 insertions(+), 45 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt index 9ec43ee9b..905954fd2 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt @@ -203,7 +203,7 @@ class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: Mu // Now that all songs are processed, also process albums and group them into their // respective artists. - val albums = albumGrouping.values.map { AlbumImpl(it.raw.inner, musicSettings, it.music) } + val albums = albumGrouping.values.map { AlbumImpl(it, musicSettings) } for (album in albums) { for (rawArtist in album.rawArtists) { val key = RawArtist.Key(rawArtist) @@ -236,19 +236,11 @@ class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: Mu } // Artists and genres do not need to be grouped and can be processed immediately. - val artists = - artistGrouping.values.map { ArtistImpl(it.raw.inner, musicSettings, it.music) } - val genres = genreGrouping.values.map { GenreImpl(it.raw.inner, musicSettings, it.music) } + val artists = artistGrouping.values.map { ArtistImpl(it, musicSettings) } + val genres = genreGrouping.values.map { GenreImpl(it, musicSettings) } return DeviceLibraryImpl(songGrouping.values, albums, artists, genres) } - - private data class Grouping( - var raw: PrioritizedRaw, - val music: MutableList - ) - - private data class PrioritizedRaw(val inner: R, val src: M) } class DeviceLibraryImpl( diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt index 82827b59b..095be1967 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt @@ -231,17 +231,16 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son /** * Library-backed implementation of [Album]. * - * @param rawAlbum The [RawAlbum] to derive the member data from. + * @param grouping [Grouping] to derive the member data from. * @param musicSettings [MusicSettings] to for user parsing configuration. - * @param songs The [Song]s that are a part of this [Album]. These items will be linked to this - * [Album]. * @author Alexander Capehart (OxygenCobalt) */ class AlbumImpl( - private val rawAlbum: RawAlbum, + grouping: Grouping, musicSettings: MusicSettings, - override val songs: List ) : Album { + private val rawAlbum = grouping.raw.inner + override val uid = // Attempt to use a MusicBrainz ID first before falling back to a hashed UID. rawAlbum.musicBrainzId?.let { Music.UID.musicBrainz(MusicMode.ALBUMS, it) } @@ -259,6 +258,7 @@ class AlbumImpl( override val durationMs: Long override val dateAdded: Long + override val songs: List private val _artists = mutableListOf() override val artists: List get() = _artists @@ -272,7 +272,7 @@ class AlbumImpl( var earliestDateAdded: Long = Long.MAX_VALUE // Do linking and value generation in the same loop for efficiency. - for (song in songs) { + for (song in grouping.music) { song.link(this) if (song.date != null) { @@ -298,6 +298,7 @@ class AlbumImpl( dates = if (min != null && max != null) Date.Range(min, max) else null durationMs = totalDuration dateAdded = earliestDateAdded + songs = Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING).songs(grouping.music) hashCode = 31 * hashCode + rawAlbum.hashCode() hashCode = 31 * hashCode + songs.hashCode() @@ -347,18 +348,13 @@ class AlbumImpl( /** * Library-backed implementation of [Artist]. * - * @param rawArtist The [RawArtist] to derive the member data from. + * @param grouping [Grouping] to derive the member data from. * @param musicSettings [MusicSettings] to for user parsing configuration. - * @param songAlbums A list of the [Song]s and [Album]s that are a part of this [Artist] , either - * through artist or album artist tags. Providing [Song]s to the artist is optional. These - * instances will be linked to this [Artist]. * @author Alexander Capehart (OxygenCobalt) */ -class ArtistImpl( - private val rawArtist: RawArtist, - musicSettings: MusicSettings, - songAlbums: List -) : Artist { +class ArtistImpl(grouping: Grouping, musicSettings: MusicSettings) : Artist { + private val rawArtist = grouping.raw.inner + override val uid = // Attempt to use a MusicBrainz ID first before falling back to a hashed UID. rawArtist.musicBrainzId?.let { Music.UID.musicBrainz(MusicMode.ARTISTS, it) } @@ -381,7 +377,7 @@ class ArtistImpl( val distinctSongs = mutableSetOf() val albumMap = mutableMapOf() - for (music in songAlbums) { + for (music in grouping.music) { when (music) { is SongImpl -> { music.link(this) @@ -398,7 +394,7 @@ class ArtistImpl( } } - songs = distinctSongs.toList() + songs = Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING).songs(distinctSongs) albums = Sort(Sort.Mode.ByDate, Sort.Direction.DESCENDING).albums(albumMap.keys) explicitAlbums = albums.filter { unlikelyToBeNull(albumMap[it]) } implicitAlbums = albums.filterNot { unlikelyToBeNull(albumMap[it]) } @@ -449,40 +445,38 @@ class ArtistImpl( /** * Library-backed implementation of [Genre]. * - * @param rawGenre [RawGenre] to derive the member data from. + * @param grouping [Grouping] to derive the member data from. * @param musicSettings [MusicSettings] to for user parsing configuration. - * @param songs Child [SongImpl]s of this instance. * @author Alexander Capehart (OxygenCobalt) */ -class GenreImpl( - private val rawGenre: RawGenre, - musicSettings: MusicSettings, - override val songs: List -) : Genre { +class GenreImpl(grouping: Grouping, musicSettings: MusicSettings) : Genre { + private val rawGenre = grouping.raw.inner + override val uid = Music.UID.auxio(MusicMode.GENRES) { update(rawGenre.name) } override val name = rawGenre.name?.let { Name.Known.from(it, rawGenre.name, musicSettings) } ?: Name.Unknown(R.string.def_genre) + override val songs: List override val artists: List override val durationMs: Long private var hashCode = uid.hashCode() init { - val distinctAlbums = mutableSetOf() val distinctArtists = mutableSetOf() var totalDuration = 0L - for (song in songs) { + for (song in grouping.music) { song.link(this) - distinctAlbums.add(song.album) distinctArtists.addAll(song.artists) totalDuration += song.durationMs } + songs = Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING).songs(grouping.music) artists = Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING).artists(distinctArtists) durationMs = totalDuration + hashCode = 31 * hashCode + rawGenre.hashCode() hashCode = 31 * hashCode + songs.hashCode() } @@ -516,3 +510,7 @@ class GenreImpl( return this } } + +data class Grouping(var raw: PrioritizedRaw, val music: MutableList) + +data class PrioritizedRaw(val inner: R, val src: M) diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt b/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt index 5828ac75a..b3037b0de 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt @@ -25,7 +25,6 @@ import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.fs.Directory import org.oxycblt.auxio.music.info.Date import org.oxycblt.auxio.music.info.ReleaseType -import org.oxycblt.auxio.util.logD /** * Raw information about a [SongImpl] obtained from the filesystem/Extractor instances. @@ -120,7 +119,10 @@ data class RawAlbum( ) { val key = Key(this) - /** Exposed information that denotes [RawAlbum] uniqueness. */ + /** + * Allows [RawAlbum]s to be compared by "fundamental" information that is unlikely to change on + * an item-by-item basis. + */ data class Key(private val inner: RawAlbum) { // Albums are grouped as follows: // - If we have a MusicBrainz ID, only group by it. This allows different Albums with the @@ -167,7 +169,7 @@ data class RawArtist( /** * Allows [RawArtist]s to be compared by "fundamental" information that is unlikely to change on - * an item-by-item + * an item-by-item basis. */ data class Key(private val inner: RawArtist) { // Artists are grouped as follows: @@ -179,10 +181,6 @@ data class RawArtist( // Cache the hashCode for HashMap efficiency. val hashCode = inner.musicBrainzId?.hashCode() ?: inner.name?.lowercase().hashCode() - init { - logD("${inner.name} ${inner.name?.lowercase().hashCode()} $hashCode") - } - // Compare names and MusicBrainz IDs in order to differentiate artists with the // same name in large libraries. @@ -216,6 +214,10 @@ data class RawGenre( ) { val key = Key(this) + /** + * Allows [RawGenre]s to be compared by "fundamental" information that is unlikely to change on + * an item-by-item basis. + */ data class Key(private val inner: RawGenre) { // Cache the hashCode for HashMap efficiency. private val hashCode = inner.name?.lowercase().hashCode() From d22de34fd38959d9d82e8c64a2852624e65898f0 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Tue, 13 Jun 2023 10:03:48 -0600 Subject: [PATCH 21/26] music: use sets for library information Use a Set when storing all song, album, artist, and genre information. These all have no consistent ordering due to parallelization, and should be communicated as such. --- .../oxycblt/auxio/music/device/DeviceLibrary.kt | 16 ++++++++-------- .../auxio/music/device/DeviceMusicImpl.kt | 4 ++-- .../org/oxycblt/auxio/music/device/RawMusic.kt | 6 +++--- .../org/oxycblt/auxio/music/user/UserLibrary.kt | 6 +++--- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt index 905954fd2..613a6473b 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt @@ -203,7 +203,7 @@ class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: Mu // Now that all songs are processed, also process albums and group them into their // respective artists. - val albums = albumGrouping.values.map { AlbumImpl(it, musicSettings) } + val albums = albumGrouping.values.mapTo(mutableSetOf()) { AlbumImpl(it, musicSettings) } for (album in albums) { for (rawArtist in album.rawArtists) { val key = RawArtist.Key(rawArtist) @@ -236,18 +236,18 @@ class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: Mu } // Artists and genres do not need to be grouped and can be processed immediately. - val artists = artistGrouping.values.map { ArtistImpl(it, musicSettings) } - val genres = genreGrouping.values.map { GenreImpl(it, musicSettings) } + val artists = artistGrouping.values.mapTo(mutableSetOf()) { ArtistImpl(it, musicSettings) } + val genres = genreGrouping.values.mapTo(mutableSetOf()) { GenreImpl(it, musicSettings) } - return DeviceLibraryImpl(songGrouping.values, albums, artists, genres) + return DeviceLibraryImpl(songGrouping.values.toSet(), albums, artists, genres) } } class DeviceLibraryImpl( - override val songs: Collection, - override val albums: Collection, - override val artists: Collection, - override val genres: Collection + override val songs: Set, + override val albums: Set, + override val artists: Set, + override val genres: Set ) : DeviceLibrary { // Use a mapping to make finding information based on it's UID much faster. private val songUidMap = buildMap { songs.forEach { put(it.uid, it.finalize()) } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt index 095be1967..ce18f6880 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt @@ -298,7 +298,7 @@ class AlbumImpl( dates = if (min != null && max != null) Date.Range(min, max) else null durationMs = totalDuration dateAdded = earliestDateAdded - songs = Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING).songs(grouping.music) + songs = Sort(Sort.Mode.ByTrack, Sort.Direction.ASCENDING).songs(grouping.music) hashCode = 31 * hashCode + rawAlbum.hashCode() hashCode = 31 * hashCode + songs.hashCode() @@ -394,7 +394,7 @@ class ArtistImpl(grouping: Grouping, musicSettings: MusicSetti } } - songs = Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING).songs(distinctSongs) + songs = Sort(Sort.Mode.ByDate, Sort.Direction.ASCENDING).songs(distinctSongs) albums = Sort(Sort.Mode.ByDate, Sort.Direction.DESCENDING).albums(albumMap.keys) explicitAlbums = albums.filter { unlikelyToBeNull(albumMap[it]) } implicitAlbums = albums.filterNot { unlikelyToBeNull(albumMap[it]) } diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt b/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt index b3037b0de..9b92046be 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt @@ -121,7 +121,7 @@ data class RawAlbum( /** * Allows [RawAlbum]s to be compared by "fundamental" information that is unlikely to change on - * an item-by-item basis. + * an item-by-item */ data class Key(private val inner: RawAlbum) { // Albums are grouped as follows: @@ -169,7 +169,7 @@ data class RawArtist( /** * Allows [RawArtist]s to be compared by "fundamental" information that is unlikely to change on - * an item-by-item basis. + * an item-by-item */ data class Key(private val inner: RawArtist) { // Artists are grouped as follows: @@ -216,7 +216,7 @@ data class RawGenre( /** * Allows [RawGenre]s to be compared by "fundamental" information that is unlikely to change on - * an item-by-item basis. + * an item-by-item */ data class Key(private val inner: RawGenre) { // Cache the hashCode for HashMap efficiency. diff --git a/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt index 0228764a3..bb1dda066 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt @@ -42,7 +42,7 @@ import org.oxycblt.auxio.util.logE */ interface UserLibrary { /** The current user-defined playlists. */ - val playlists: List + val playlists: Collection /** * Find a [Playlist] instance corresponding to the given [Music.UID]. @@ -173,8 +173,8 @@ private class UserLibraryImpl( override fun equals(other: Any?) = other is UserLibraryImpl && other.playlistMap == playlistMap override fun toString() = "UserLibrary(playlists=${playlists.size})" - override val playlists: List - get() = playlistMap.values.toList() + override val playlists: Collection + get() = playlistMap.values.toSet() override fun findPlaylist(uid: Music.UID) = playlistMap[uid] From 31d647123f11256e55b1efe1ad5139fce286fb7d Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Tue, 13 Jun 2023 10:40:55 -0600 Subject: [PATCH 22/26] music: use set for child information Use sets for all child music information. Unlike parent information, which usually has an ordering derived from file information, child music information more or less doesn't, and will be consistently re-interpreted by the app to apply user-configured sorts. --- .../oxycblt/auxio/detail/DetailViewModel.kt | 11 ++++-- .../java/org/oxycblt/auxio/image/CoverView.kt | 2 +- .../auxio/image/extractor/Components.kt | 10 +++--- .../auxio/image/extractor/CoverExtractor.kt | 12 ++++--- .../oxycblt/auxio/image/extractor/CoverUri.kt | 31 ++++++++++++++++ .../java/org/oxycblt/auxio/music/Music.kt | 17 +++++---- .../auxio/music/device/DeviceLibrary.kt | 8 ++--- .../auxio/music/device/DeviceMusicImpl.kt | 35 +++++++++---------- .../oxycblt/auxio/music/device/RawMusic.kt | 18 ++++++++++ .../playback/system/MediaSessionComponent.kt | 2 +- .../org/oxycblt/auxio/util/FrameworkUtil.kt | 2 +- 11 files changed, 100 insertions(+), 48 deletions(-) create mode 100644 app/src/main/java/org/oxycblt/auxio/image/extractor/CoverUri.kt diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index f18321430..c63c76151 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -480,7 +480,7 @@ constructor( // implicit album list into the mapping. logD("Implicit albums present, adding to list") @Suppress("UNCHECKED_CAST") - (grouping as MutableMap>)[AlbumGrouping.APPEARANCES] = + (grouping as MutableMap>)[AlbumGrouping.APPEARANCES] = artist.implicitAlbums } @@ -490,7 +490,7 @@ constructor( val header = BasicHeader(entry.key.headerTitleRes) list.add(Divider(header)) list.add(header) - list.addAll(entry.value) + list.addAll(ARTIST_ALBUM_SORT.albums(entry.value)) } // Artists may not be linked to any songs, only include a header entry if we have any. @@ -519,7 +519,7 @@ constructor( val artistHeader = BasicHeader(R.string.lbl_artists) list.add(Divider(artistHeader)) list.add(artistHeader) - list.addAll(genre.artists) + list.addAll(GENRE_ARTIST_SORT.artists(genre.artists)) val songHeader = SortHeader(R.string.lbl_songs) list.add(Divider(songHeader)) @@ -576,4 +576,9 @@ constructor( LIVE(R.string.lbl_live_group), REMIXES(R.string.lbl_remix_group), } + + private companion object { + val ARTIST_ALBUM_SORT = Sort(Sort.Mode.ByDate, Sort.Direction.DESCENDING) + val GENRE_ARTIST_SORT = Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING) + } } diff --git a/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt b/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt index a4d0e6917..b2912595c 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt @@ -379,7 +379,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr * @param desc The content description to describe the bound data. * @param errorRes The resource of the error drawable to use if the cover cannot be loaded. */ - fun bind(songs: List, desc: String, @DrawableRes errorRes: Int) { + fun bind(songs: Collection, desc: String, @DrawableRes errorRes: Int) { val request = ImageRequest.Builder(context) .data(songs) diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt index 017a76747..b7a7183db 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt @@ -27,22 +27,22 @@ import javax.inject.Inject import org.oxycblt.auxio.music.Song class SongKeyer @Inject constructor(private val coverExtractor: CoverExtractor) : - Keyer> { - override fun key(data: List, options: Options) = + Keyer> { + override fun key(data: Collection, options: Options) = "${coverExtractor.computeCoverOrdering(data).hashCode()}" } class SongCoverFetcher private constructor( - private val songs: List, + private val songs: Collection, private val size: Size, private val coverExtractor: CoverExtractor, ) : Fetcher { override suspend fun fetch() = coverExtractor.extract(songs, size) class Factory @Inject constructor(private val coverExtractor: CoverExtractor) : - Fetcher.Factory> { - override fun create(data: List, options: Options, imageLoader: ImageLoader) = + Fetcher.Factory> { + override fun create(data: Collection, options: Options, imageLoader: ImageLoader) = SongCoverFetcher(data, options.size, coverExtractor) } } diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt index 067b8361c..6ca126256 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt @@ -77,7 +77,7 @@ constructor( * will be returned of a mosaic composed of four album covers ordered by * [computeCoverOrdering]. Otherwise, a [SourceResult] of one album cover will be returned. */ - suspend fun extract(songs: List, size: Size): FetchResult? { + suspend fun extract(songs: Collection, size: Size): FetchResult? { val albums = computeCoverOrdering(songs) val streams = mutableListOf() for (album in albums) { @@ -117,7 +117,7 @@ constructor( * by their names. "Representation" is defined by how many [Song]s were found to be linked to * the given [Album] in the given [Song] list. */ - fun computeCoverOrdering(songs: List): List { + fun computeCoverOrdering(songs: Collection): List { // TODO: Start short-circuiting in more places if (songs.isEmpty()) return listOf() if (songs.size == 1) return listOf(songs.first().album) @@ -150,7 +150,7 @@ constructor( MediaMetadataRetriever().run { // This call is time-consuming but it also doesn't seem to hold up the main thread, // so it's probably fine not to wrap it.rmt - setDataSource(context, album.songs[0].uri) + setDataSource(context, album.coverUri.song) // Get the embedded picture from MediaMetadataRetriever, which will return a full // ByteArray of the cover without any compression artifacts. @@ -161,7 +161,7 @@ constructor( private suspend fun extractExoplayerCover(album: Album): InputStream? { val tracks = MetadataRetriever.retrieveMetadata( - mediaSourceFactory, MediaItem.fromUri(album.songs[0].uri)) + mediaSourceFactory, MediaItem.fromUri(album.coverUri.song)) .asDeferred() .await() @@ -207,7 +207,9 @@ constructor( private suspend fun extractMediaStoreCover(album: Album) = // Eliminate any chance that this blocking call might mess up the loading process - withContext(Dispatchers.IO) { context.contentResolver.openInputStream(album.coverUri) } + withContext(Dispatchers.IO) { + context.contentResolver.openInputStream(album.coverUri.mediaStore) + } /** Derived from phonograph: https://github.com/kabouzeid/Phonograph */ private fun createMosaic(streams: List, size: Size): FetchResult { diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverUri.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverUri.kt new file mode 100644 index 000000000..4aadf932e --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverUri.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 Auxio Project + * CoverUri.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.auxio.image.extractor + +import android.net.Uri + +/** + * Bundle of [Uri] information used in [CoverExtractor] to ensure consistent [Uri] use when loading + * images. + * @param mediaStore The album cover [Uri] obtained from MediaStore. + * @param song The [Uri] of the first song (by track) of the album, which can also be used to + * obtain an album cover. + * @author Alexander Capehart (OxygenCobalt) + */ +data class CoverUri(val mediaStore: Uri, val song: Uri) 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 395b17d93..fc8a51390 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -27,6 +27,7 @@ import java.util.UUID import kotlin.math.max import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize +import org.oxycblt.auxio.image.extractor.CoverUri import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.music.fs.MimeType import org.oxycblt.auxio.music.fs.Path @@ -225,7 +226,7 @@ sealed interface Music : Item { */ sealed interface MusicParent : Music { /** The child [Song]s of this [MusicParent]. */ - val songs: List + val songs: Collection } /** @@ -296,7 +297,7 @@ interface Album : MusicParent { * The URI to a MediaStore-provided album cover. These images will be fast to load, but at the * cost of image quality. */ - val coverUri: Uri + val coverUri: CoverUri /** The duration of all songs in the album, in milliseconds. */ val durationMs: Long /** The earliest date a song in this album was added, as a unix epoch timestamp. */ @@ -321,14 +322,11 @@ interface Artist : MusicParent { * Note that any [Song] credited to this artist will have it's [Album] considered to be * "indirectly" linked to this [Artist], and thus included in this list. */ - val albums: List - + val albums: Collection /** Albums directly credited to this [Artist] via a "Album Artist" tag. */ - val explicitAlbums: List - + val explicitAlbums: Collection /** Albums indirectly credited to this [Artist] via an "Artist" tag. */ - val implicitAlbums: List - + val implicitAlbums: Collection /** * The duration of all [Song]s in the artist, in milliseconds. Will be null if there are no * songs. @@ -345,7 +343,7 @@ interface Artist : MusicParent { */ interface Genre : MusicParent { /** The artists indirectly linked to by the [Artist]s of this [Genre]. */ - val artists: List + val artists: Collection /** The total duration of the songs in this genre, in milliseconds. */ val durationMs: Long } @@ -356,6 +354,7 @@ interface Genre : MusicParent { * @author Alexander Capehart (OxygenCobalt) */ interface Playlist : MusicParent { + override val songs: List /** The total duration of the songs in this genre, in milliseconds. */ val durationMs: Long } diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt index 613a6473b..5a70dc578 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt @@ -160,7 +160,7 @@ class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: Mu } else { // Need to initialize this grouping. albumGrouping[albumKey] = - Grouping(PrioritizedRaw(song.rawAlbum, song), mutableListOf(song)) + Grouping(PrioritizedRaw(song.rawAlbum, song), mutableSetOf(song)) } // Group the song into each of it's artists. @@ -174,7 +174,7 @@ class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: Mu } else { // Need to initialize this grouping. artistGrouping[artistKey] = - Grouping(PrioritizedRaw(rawArtist, song), mutableListOf(song)) + Grouping(PrioritizedRaw(rawArtist, song), mutableSetOf(song)) } } @@ -194,7 +194,7 @@ class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: Mu } else { // Need to initialize this grouping. genreGrouping[genreKey] = - Grouping(PrioritizedRaw(rawGenre, song), mutableListOf(song)) + Grouping(PrioritizedRaw(rawGenre, song), mutableSetOf(song)) } } @@ -230,7 +230,7 @@ class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: Mu } else { // Need to initialize this grouping. artistGrouping[key] = - Grouping(PrioritizedRaw(rawArtist, album), mutableListOf(album)) + Grouping(PrioritizedRaw(rawArtist, album), mutableSetOf(album)) } } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt index ce18f6880..9cb4d70b5 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt @@ -19,6 +19,7 @@ package org.oxycblt.auxio.music.device import org.oxycblt.auxio.R +import org.oxycblt.auxio.image.extractor.CoverUri import org.oxycblt.auxio.list.Sort import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist @@ -254,15 +255,16 @@ class AlbumImpl( override val name = Name.Known.from(rawAlbum.name, rawAlbum.sortName, musicSettings) override val dates: Date.Range? override val releaseType = rawAlbum.releaseType ?: ReleaseType.Album(null) - override val coverUri = rawAlbum.mediaStoreId.toCoverUri() + override val coverUri = CoverUri(rawAlbum.mediaStoreId.toCoverUri(), grouping.raw.src.uri) override val durationMs: Long override val dateAdded: Long - override val songs: List private val _artists = mutableListOf() override val artists: List get() = _artists + override val songs: Set = grouping.music + private var hashCode = uid.hashCode() init { @@ -298,7 +300,6 @@ class AlbumImpl( dates = if (min != null && max != null) Date.Range(min, max) else null durationMs = totalDuration dateAdded = earliestDateAdded - songs = Sort(Sort.Mode.ByTrack, Sort.Direction.ASCENDING).songs(grouping.music) hashCode = 31 * hashCode + rawAlbum.hashCode() hashCode = 31 * hashCode + songs.hashCode() @@ -363,10 +364,10 @@ class ArtistImpl(grouping: Grouping, musicSettings: MusicSetti rawArtist.name?.let { Name.Known.from(it, rawArtist.sortName, musicSettings) } ?: Name.Unknown(R.string.def_artist) - override val songs: List - override val albums: List - override val explicitAlbums: List - override val implicitAlbums: List + override val songs: Set + override val albums: Set + override val explicitAlbums: Set + override val implicitAlbums: Set override val durationMs: Long? override lateinit var genres: List @@ -394,10 +395,10 @@ class ArtistImpl(grouping: Grouping, musicSettings: MusicSetti } } - songs = Sort(Sort.Mode.ByDate, Sort.Direction.ASCENDING).songs(distinctSongs) - albums = Sort(Sort.Mode.ByDate, Sort.Direction.DESCENDING).albums(albumMap.keys) - explicitAlbums = albums.filter { unlikelyToBeNull(albumMap[it]) } - implicitAlbums = albums.filterNot { unlikelyToBeNull(albumMap[it]) } + songs = distinctSongs + albums = albumMap.keys + explicitAlbums = albums.filterTo(mutableSetOf()) { albumMap[it] == true } + implicitAlbums = albums.filterNotTo(mutableSetOf()) { albumMap[it] == true } durationMs = songs.sumOf { it.durationMs }.nonZeroOrNull() hashCode = 31 * hashCode + rawArtist.hashCode() @@ -457,8 +458,8 @@ class GenreImpl(grouping: Grouping, musicSettings: MusicSett rawGenre.name?.let { Name.Known.from(it, rawGenre.name, musicSettings) } ?: Name.Unknown(R.string.def_genre) - override val songs: List - override val artists: List + override val songs: Set + override val artists: Set override val durationMs: Long private var hashCode = uid.hashCode() @@ -473,8 +474,8 @@ class GenreImpl(grouping: Grouping, musicSettings: MusicSett totalDuration += song.durationMs } - songs = Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING).songs(grouping.music) - artists = Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING).artists(distinctArtists) + songs = grouping.music + artists = distinctArtists durationMs = totalDuration hashCode = 31 * hashCode + rawGenre.hashCode() @@ -510,7 +511,3 @@ class GenreImpl(grouping: Grouping, musicSettings: MusicSett return this } } - -data class Grouping(var raw: PrioritizedRaw, val music: MutableList) - -data class PrioritizedRaw(val inner: R, val src: M) diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt b/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt index 9b92046be..73fa3c753 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt @@ -237,3 +237,21 @@ data class RawGenre( } } } + +/** + * Represents grouped music information and the prioritized raw information to eventually derive a + * [Music] implementation instance from. + * + * @param raw The current [PrioritizedRaw] that will be used for the finalized music information. + * @param music The child [Music] instances of the music information to be created. + */ +data class Grouping(var raw: PrioritizedRaw, val music: MutableSet) + +/** + * Represents a [RawAlbum], [RawArtist], or [RawGenre] specifically chosen to create a [Music] + * instance from due to it being the most likely source of truth. + * + * @param inner The raw music instance that will be used. + * @param src The [Music] instance that the raw information was derived from. + */ +data class PrioritizedRaw(val inner: R, val src: M) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt index 9d273ac98..9acbe82d0 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt @@ -367,7 +367,7 @@ constructor( .setSubtitle(song.artists.resolveNames(context)) // Since we usually have to load many songs into the queue, use the // MediaStore URI instead of loading a bitmap. - .setIconUri(song.album.coverUri) + .setIconUri(song.album.coverUri.mediaStore) .setMediaUri(song.uri) .build() // Store the item index so we can then use the analogous index in the diff --git a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt index eb24d8093..d06c0ca37 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt @@ -286,7 +286,7 @@ fun Context.share(parent: MusicParent) = share(parent.songs) * * @param songs The [Song]s to share. */ -fun Context.share(songs: List) { +fun Context.share(songs: Collection) { if (songs.isEmpty()) return logD("Showing sharesheet for ${songs.size} songs") val builder = ShareCompat.IntentBuilder(this) From f734268b40fb51b6b8eb5c55a237c361a6175317 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Thu, 15 Jun 2023 19:10:53 +0200 Subject: [PATCH 23/26] Translations update from Hosted Weblate (#460) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Translated using Weblate (Russian) Currently translated at 100.0% (278 of 278 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/ru/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (278 of 278 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/uk/ * Translated using Weblate (Finnish) Currently translated at 92.8% (258 of 278 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/fi/ * Translated using Weblate (Finnish) Currently translated at 96.8% (31 of 32 strings) Translation: Auxio/Metadata Translate-URL: https://hosted.weblate.org/projects/auxio/metadata/fi/ * Translated using Weblate (Italian) Currently translated at 98.5% (274 of 278 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/it/ * Translated using Weblate (Italian) Currently translated at 99.2% (276 of 278 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/it/ * Translated using Weblate (Croatian) Currently translated at 100.0% (278 of 278 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/hr/ * Translated using Weblate (Korean) Currently translated at 100.0% (33 of 33 strings) Translation: Auxio/Metadata Translate-URL: https://hosted.weblate.org/projects/auxio/metadata/ko/ * Translated using Weblate (Turkish) Currently translated at 94.2% (262 of 278 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/tr/ * Translated using Weblate (German) Currently translated at 100.0% (278 of 278 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/de/ * Translated using Weblate (French) Currently translated at 97.1% (270 of 278 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/fr/ * Translated using Weblate (Italian) Currently translated at 99.2% (276 of 278 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/it/ * Translated using Weblate (Punjabi) Currently translated at 100.0% (278 of 278 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/pa/ * Translated using Weblate (Punjabi) Currently translated at 100.0% (33 of 33 strings) Translation: Auxio/Metadata Translate-URL: https://hosted.weblate.org/projects/auxio/metadata/pa/ * Translated using Weblate (Hindi) Currently translated at 100.0% (33 of 33 strings) Translation: Auxio/Metadata Translate-URL: https://hosted.weblate.org/projects/auxio/metadata/hi/ * Translated using Weblate (French) Currently translated at 100.0% (278 of 278 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/fr/ * Translated using Weblate (Italian) Currently translated at 100.0% (278 of 278 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/it/ * Translated using Weblate (Hindi) Currently translated at 61.5% (171 of 278 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/hi/ * Translated using Weblate (Dutch) Currently translated at 100.0% (278 of 278 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/nl/ * Translated using Weblate (Croatian) Currently translated at 100.0% (33 of 33 strings) Translation: Auxio/Metadata Translate-URL: https://hosted.weblate.org/projects/auxio/metadata/hr/ --------- Co-authored-by: ะœะฐะบะฐั€ ะ ะฐะทะธะฝ Co-authored-by: BMT[UA] Co-authored-by: Jiri Grรถnroos Co-authored-by: atilluF <110931720+atilluF@users.noreply.github.com> Co-authored-by: Milo Ivir Co-authored-by: Hoseok Seo Co-authored-by: Baki Nazik Co-authored-by: J. Lavoie Co-authored-by: ShareASmile Co-authored-by: Issa1553 --- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values-fi/strings.xml | 30 ++- app/src/main/res/values-fr/strings.xml | 97 ++++++++-- app/src/main/res/values-hi/strings.xml | 128 +++++++++++++ app/src/main/res/values-hr/strings.xml | 16 +- app/src/main/res/values-it/strings.xml | 32 ++-- app/src/main/res/values-nl/strings.xml | 175 ++++++++++++++---- app/src/main/res/values-pa/strings.xml | 91 ++++++++- app/src/main/res/values-ru/strings.xml | 2 +- app/src/main/res/values-tr/strings.xml | 5 + app/src/main/res/values-uk/strings.xml | 2 +- .../metadata/android/fi/short_description.txt | 1 + .../metadata/android/hi/full_description.txt | 3 +- .../metadata/android/hr/full_description.txt | 37 ++-- .../metadata/android/ko/full_description.txt | 12 +- .../metadata/android/pa/full_description.txt | 5 +- 16 files changed, 532 insertions(+), 106 deletions(-) create mode 100644 fastlane/metadata/android/fi/short_description.txt diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index e70e04dfd..03ca85350 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -169,7 +169,7 @@ Matroska-Audio Advanced Audio Coding (AAC) Free Lossless Audio Codec (FLAC) - %d kbps + %d kB/s %d Hz Lade deine Musikbibliothekโ€ฆ (%1$d/%2$d) Mischen diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 85ff6f8e5..62053b8a5 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -22,7 +22,7 @@ Tyylilaji Tyylilajit Soittolista - Mix + DJ-mix Live Soittolistat Etsi @@ -164,7 +164,7 @@ Remix-albumi Remix-EP Remix-single - Remix-kokoelmat + Remix-kokoelma Ladataan musiikkikirjastoaโ€ฆ Versio Vรคriteema @@ -182,7 +182,7 @@ Kรคytรค mustaa teemaa Pyรถristetty tila Elokuvamusiikit - Mixaukset + DJ-mixaukset Auxio tarvitsee luvan lukea musiikkikirjastoa Asetukset Jรคrjestรค @@ -243,4 +243,28 @@ Palauta aiemmin tallennettu toiston tila (jos olemassa) Musiikkia ei ladata valitsemistasi kansioista. Suosi albumia, jos sellaista toistetaan + Uusi soittolista + Soittolista %d + Lisรครค soittolistaan + Ei kappaleita + Ota kรคyttรถรถn pyรถristetyt reunat kรคyttรถliittymรคn lisรคelementeissรค (vaatii albumikansien olevan pyรถristettyjรค) + Lataa musiikkikirjasto uudelleen sen muuttuessa (vaatii pysyvรคn ilmoituksen) + Kauttaviiva (/) + Poista + Nimeรค uudelleen + Nimeรค soittolista uudelleen + Poistetaanko soittolista\? + Muokataan %s + Muokkaa + Poista tรคmรค kappale + Soittolista luotu + Soittolista nimetty uudelleen + Soittolista poistettu + Lisรคtty soittolistaan + Jaa + Esiintyy + Nรคytรค ja hallinnoi musiikin toistoa + Siirrรค tรคmรค kappale + Ei levyรค + Poistetaanko %s\? Tรคtรค toimenpidettรค ei voi perua. \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 353726d0a..55adf537f 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -41,7 +41,7 @@ Pas de musique trouvรฉe Morceau %d - Lecture/Pause + Lecture ou pause Recherche dans votre bibliothรจqueโ€ฆ @@ -50,26 +50,26 @@ Violet Indigo Bleu - Bleu Clair + Bleu foncรฉ Bleu Vert Vert - Vert Clair + Vert foncรฉ Vert Citron Jaune Orange Brun Gris - Titres chargรฉs: %d + Titres chargรฉsโ€ฏ: %d - %s Titre - %s Titres - %s Titres + %d titre + %d titres + %d titres - %s Album - %s Albums - %s Albums + %d album + %d albums + %d albums Format ร‰tat sauvegardรฉ @@ -92,7 +92,7 @@ Affichage Onglets de la bibliothรจque Un lecteur de musique simple et rationnel pour Android. - Chargement de musique + Chargement de la musique Afficher et contrรดler la lecture de la musique Chargement de votre bibliothรจque musicaleโ€ฆ Nom @@ -106,7 +106,7 @@ Voir les propriรฉtรฉs Propriรฉtรฉs de la chanson EP live - EP de remixes + EP de remix Single live Single remixรฉ Compilations @@ -114,7 +114,7 @@ Live Chargement de la musique Suivre la librairie musicale - EPs + EP EP Singles Single @@ -125,7 +125,7 @@ Remix Date d\'ajout Album live - Album de remixes + Album de remix Genre ร‰galiseur Lecture alรฉatoire de tous les titres @@ -140,9 +140,9 @@ Supprimer le dossier Artiste inconnu Compilation en direct - Compilations de remix - Mixes - Mix + Compilation de remix + Mix DJ + Mix DJ Ce dossier n\'est pas pris en charge Rรฉinitialiser Ogg audio @@ -194,7 +194,7 @@ Virgule (,) Point-virgule (;) Ignorer les fichiers audio qui ne sont pas de la musique, tels que les podcasts - Avertissement: L\'utilisation de ce paramรจtre peut entraรฎner l\'interprรฉtation incorrecte de certaines balises comme ayant plusieurs valeurs. Vous pouvez rรฉsoudre ce problรจme en prรฉfixant les caractรจres de sรฉparation indรฉsirables avec une barre oblique inverse (\\). + Avertissementโ€ฏ: L\'utilisation de ce paramรจtre peut entraรฎner l\'interprรฉtation incorrecte de certaines balises comme ayant plusieurs valeurs. Vous pouvez rรฉsoudre ce problรจme en prรฉfixant les caractรจres de sรฉparation indรฉsirables avec une barre oblique inverse (\\). Exclure non-musique Lire depuis l\'album Barre oblique (/) @@ -236,4 +236,65 @@ Recharger la bibliothรจque musicale en utilisant si possible les รฉtiquettes en cache Mode Exclure + Nouvelle liste de lecture + Passer ร  la chanson suivante + Activer ou dรฉsactiver la lecture alรฉatoire + %d Hz + Passer ร  la derniรจre chanson + Ajouter ร  la liste de lecture + Crรฉer une nouvelle liste de lecture + Audio Matroska + Artistes chargรฉs&nbsp;: %d + Rembobiner avant de revenir en arriรจre + Image d\'artiste pour %s + Aucune piste + Aucune musique en cours de lecture + Supprimer + Pause quand une chanson se rรฉpรจte + Dรฉplacer cet onglet + Renommer + Impossible d\'effacer l\'รฉtat + Modifier le mode de rรฉpรฉtition + Albums chargรฉs&nbsp;: %d + Durรฉe totale&nbsp;: %s + Effacer la requรชte de recherche + Image de la liste de lecture pour %s + Disque %d + Chargement de votre bibliothรจque musicaleโ€ฆ (%1$d/%2$d) + + %d artiste + %d artistes + %d artistes + + Modifier + Pause en cas de rรฉpรฉtition + Rembobiner avant de passer ร  la chanson prรฉcรฉdente + Arrรชter la lecture + Liste de lecture crรฉรฉe + Liste de lecture renommรฉe + Liste de lecture supprimรฉe + Ajoutรฉ ร  la liste de lecture + %d ko/s + +%.1f dB + -%.1f dB + Supprimer %s&nbsp;\? Cette opรฉration ne peut pas รชtre annulรฉe. + Avertissement&nbsp;: Le fait de rรฉgler le prรฉamplificateur sur une valeur positive รฉlevรฉe peut entraรฎner l\'apparition des distortions sur certaines pistes audio. + Liste de lecture %d + Apparaรฎt sur + Renommer la liste de lecture + Supprimer la liste de lecture&nbsp;\? + Partager + Retirer cette chanson + Dรฉplacer cette chanson + Ouvrir la file d\'attente + Impossible de sauvegarder l\'รฉtat + Aucune chanson + Modification de %s + Genres chargรฉs&nbsp;: %d + Image de genre pour %s + Codec audio gratuit sans perte (FLAC) + %d sรฉlectionnรฉs + Codage audio avancรฉ (AAC) + Aucun disque + %1$s, %2$s \ No newline at end of file diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 157e025f0..2228fd6eb 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -59,4 +59,132 @@ เค—เคพเคจเฅ‡ เคฒเฅ‹เคก เคนเฅ‹ เคฐเคนเฅ‡ เคนเฅˆ เค—เคพเคจเฅ‡ เคฒเฅ‹เคก เคนเฅ‹ เคฐเคนเฅ‡ เคนเฅˆ เคเค‚เคกเฅเคฐเฅ‰เคฏเคก เค•เฅ‡ เคฒเคฟเค เคเค• เคธเฅ€เคงเคพ เคธเคพเคงเคพ, เคตเคฟเคตเฅ‡เค•เคถเฅ€เคฒ เค—เคพเคจเฅ‡ เคฌเคœเคพเคจเฅ‡ เคตเคพเคฒเคพ เคเคชเฅค + เคจเคˆ เคชเฅเคฒเฅ‡เคฒเคฟเคธเฅเคŸ + เค…เค—เคฒเคพ เคšเคฒเคพเคเค‚ + เคซเคผเคพเค‡เคฒ เค•เคพ เคจเคพเคฎ + เคฒเคพเคฏเคฌเฅเคฐเฅ‡เคฐเฅ€ เคŸเฅˆเคฌเฅเคธ + เคเคฒเฅเคฌเคฎ เคธเฅ‡ เคšเคฒเคพเคเค‚ + เคธเคพเคฎเค—เฅเคฐเฅ€ + %d เคšเคฏเคจเคฟเคค + เคชเฅเคฐเคพเคฐเฅ‚เคช + เคชเฅเคฒเฅ‡เคฒเคฟเคธเฅเคŸ เคฎเฅ‡เค‚ เคœเฅ‹เคกเคผเฅ‡เค‚ + เคฎเฅเค–เฅเคฏ เคชเคฅ + เคฌเคฟเคŸ-เคฐเฅ‡เคŸ + เคฐเคฆเฅเคฆ เค•เคฐเฅ‡เค‚ + เคธเคนเฅ‡เคœเฅ‡เค‚ + เคเคชเฅเคฒเคฟเค•เฅ‡เคถเคจ เค•เฅ€ เคฅเฅ€เคฎ เค”เคฐ เคฐเค‚เค— เคฌเคฆเคฒเฅ‡เค‚ + เคชเฅเคฒเฅ‡เคฒเคฟเคธเฅเคŸ เคฎเฅ‡เค‚ เคœเฅ‹เคกเคผเคพ เค—เคฏเคพ + เค…เค—เคฒเฅ‡ เคชเคฐ เคœเคพเคเค‚ + เคฐเคฟเคชเฅ€เคŸ เคฎเฅ‹เคก + เคธเค‚เค—เฅ€เคค เค”เคฐ เค›เคตเคฟเคฏเฅ‹เค‚ เค•เฅ‹ เคฒเฅ‹เคก เค•เคฐเคจเฅ‡ เค•เฅ‡ เคคเคฐเฅ€เค•เฅ‡ เค•เฅ‹ เคจเคฟเคฏเค‚เคคเฅเคฐเคฟเคค เค•เคฐเฅ‡เค‚ + เค—เฅˆเคฐ-เคธเค‚เค—เฅ€เคค เค•เฅ‹ เคฌเคพเคนเคฐ เคฐเค–เฅ‡เค‚ + เคธเฅเคตเคšเคพเคฒเคฟเคค เคชเฅเคจเคƒ เคฒเฅ‹เคกเคฟเค‚เค— + +%.1f dB + เคชเฅเคฒเฅ‡เคฒเคฟเคธเฅเคŸ %d + %1$s, %2$s + เค—เคคเคฟเคถเฅ€เคฒ + เคฒเฅเค• เค”เคฐ เคซเฅ€เคฒ + เค…เคคเคฟเคฐเคฟเค•เฅเคค UI เคคเคคเฅเคตเฅ‹เค‚ เคชเคฐ เค—เฅ‹เคฒ เค•เฅ‹เคจเฅ‹เค‚ เค•เฅ‹ เคธเค•เฅเคทเคฎ เค•เคฐเฅ‡เค‚ (เคเคฒเฅเคฌเคฎ เค•เคตเคฐ เค•เฅ‹ เค—เฅ‹เคฒ เค•เคฐเคจเฅ‡ เค•เฅ€ เค†เคตเคถเฅเคฏเค•เคคเคพ เคนเฅˆ) + เคฆเคฟเค–เคพเค เค—เค เค†เค‡เคŸเคฎ เคธเฅ‡ เคšเคฒเคพเคเค + เคฒเคพเค‡เคฌเฅเคฐเฅ‡เคฐเฅ€ เคธเฅ‡ เคšเคฒเคพเคคเฅ‡ เคธเคฎเคฏ + เคธเค‚เค—เฅ€เคค เคฒเคพเค‡เคฌเฅเคฐเฅ‡เคฐเฅ€ เค•เฅ‹ เคซเคฟเคฐ เคธเฅ‡ เคฒเฅ‹เคก เค•เคฐเฅ‡เค‚ เคœเคฌ เคญเฅ€ เคฏเคน เคฌเคฆเคฒเคคเคพ เคนเฅˆ (เคธเฅเคฅเคพเคˆ เคจเฅ‹เคŸเฅ€เคซเคฟเค•เฅ‡เคถเคจ เค•เฅ€ เค†เคตเคถเฅเคฏเค•เคคเคพ เคนเฅ‹เคคเฅ€ เคนเฅˆ) + เค‘เคกเคฟเคฏเฅ‹ เคซเคผเคพเค‡เคฒเฅ‹เค‚ เค•เฅ‹ เค…เคจเคฆเฅ‡เค–เคพ เค•เคฐเฅ‡เค‚ เคœเฅ‹ เคธเค‚เค—เฅ€เคค เคจเคนเฅ€เค‚ เคนเฅˆเค‚, เคœเฅˆเคธเฅ‡ เค•เคฟ เคชเฅ‰เคกเค•เคพเคธเฅเคŸ + เคฒเคพเค‡เคต เคธเค‚เค•เคฒเคจ + เคฐเฅ€เคฎเคฟเค•เฅเคธ เคธเค‚เค•เคฒเคจ + เคธเค‚เคธเฅเค•เคฐเคฃ + เคธเคญเฅ€ เคถเคซเคฒ เค•เคฐเฅ‡เค‚ + เคชเฅเคฒเฅ‡เคฒเคฟเคธเฅเคŸ + เคชเฅเคฒเฅ‡เคฒเคฟเคธเฅเคŸเฅ‡เค‚ + เค—เฅ‹เคฒ เคฎเฅ‹เคก + เคธเคญเฅ€ เค—เฅ€เคคเฅ‹เค‚ เคธเฅ‡ เคšเคฒเคพเคเค‚ + %s เคนเคŸเคพเคเค\? เค‡เคธเฅ‡ เคชเฅ‚เคฐเฅเคตเคตเคค เคจเคนเฅ€เค‚ เค•เคฟเคฏเคพ เคœเคพ เคธเค•เคคเคพเฅค + เคฒเฅ‹เคก เค•เคฟเค เค—เค เค—เคพเคจเฅ‡: %d + เค…เคตเคฐเฅ‹เคนเฅ€ + เคšเคฏเคจเคฟเคค เคšเคฒเคพเคเค + เคซเฅ‡เคฐเคฌเคฆเคฒ เค•เคพ เคšเคฏเคจ เค•เคฟเคฏเคพ เค—เคฏเคพ + เคธเฅเคฅเคฟเคคเคฟ เคธเคพเคซ เค•เฅ€ เค—เคˆ + เคธเฅเคฅเคฟเคคเคฟ เคธเคนเฅ‡เคœเฅ€ เค—เคˆ + เคฒเคพเคฏเคฌเฅเคฐเฅ‡เคฐเฅ€ เคŸเฅˆเคฌ เค•เฅ€ เคฆเฅƒเคถเฅเคฏเคคเคพ เค”เคฐ เค•เฅเคฐเคฎ เคฌเคฆเคฒเฅ‡เค‚ + เคธเค‚เค—เฅ€เคค + UI เคจเคฟเคฏเค‚เคคเฅเคฐเคฃ เค”เคฐ เคตเฅเคฏเคตเคนเคพเคฐ เค…เคจเฅเค•เฅ‚เคฒเคฟเคค เค•เคฐเฅ‡เค‚ + เค•เคฒเคพเค•เคพเคฐ เคฒเฅ‹เคก เค•เคฟเค เค—เค: %d + เค•เคธเฅเคŸเคฎ เคชเฅเคฒเฅ‡เคฌเฅˆเค• เคฌเคพเคฐ เคเค•เฅเคถเคจ + เค†เค‡เคŸเคฎ เคตเคฟเคตเคฐเคฃ เคธเฅ‡ เคšเคฒเคพเคคเฅ‡ เคธเคฎเคฏ + เคฒเคพเค‡เคต เคเคฒเฅเคฌเคฎ + เคฐเฅ€เคฎเคฟเค•เฅเคธ เคเคฒเฅเคฌเคฎ + เคฒเคพเค‡เคต EP + เคฐเฅ€เคฎเคฟเค•เฅเคธ EP + เคฒเคพเค‡เคต เคธเคฟเค‚เค—เคฒ + เคฐเฅ€เคฎเคฟเค•เฅเคธ เคธเคฟเค‚เค—เคฒ + เคธเค‚เค•เคฒเคจ + เคชเฅเคฒเฅ‡เคฒเคฟเคธเฅเคŸ เคนเคŸเคพเคเค\? + เคฌเฅเคฒเฅˆเค• เคฅเฅ€เคฎ + เคธเค‚เค—เฅ€เคค เคชเฅเคฒเฅ‡เคฌเฅˆเค• เคฆเฅ‡เค–เฅ‡เค‚ เค”เคฐ เคจเคฟเคฏเค‚เคคเฅเคฐเคฟเคค เค•เคฐเฅ‡เค‚ + เคฐเฅ€เคธเฅ‡เคŸ + เคตเคฟเค•เฅ€ + เคฒเคพเค‡เคฌเฅเคฐเฅ‡เคฐเฅ€ เค•เฅ‡ เค†เค‚เค•เคกเคผเฅ‡ + เค•เฅเคฒ เค…เคตเคงเคฟ: %s + เค‡เค•เฅเคตเคฒเคพเค‡เคœเคผเคฐ + เคเค• เคถเฅเคฆเฅเคง-เค•เคพเคฒเฅ‡ เคกเคพเคฐเฅเค• เคฅเฅ€เคฎ เค•เคพ เค‰เคชเคฏเฅ‹เค— เค•เคฐเฅ‡เค‚ + %d Hz + เคชเคฐเคฟเคตเคฐเฅเคคเคจเฅ‹เค‚ เค•เฅ‡ เคฒเคฟเค เค†เคชเค•เฅ€ เคธเค‚เค—เฅ€เคค เคฒเคพเค‡เคฌเฅเคฐเฅ‡เคฐเฅ€ เค•เฅ€ เคจเคฟเค—เคฐเคพเคจเฅ€โ€ฆ + เคˆเคชเฅ€ + เคเค•เคฒ + เคเค•เคฒ + เคเคฒเฅเคฌเคฎ + เคธเคพเค‰เค‚เคกเคŸเฅเคฐเฅˆเค• + เคธเคพเค‰เค‚เคกเคŸเฅเคฐเฅˆเค•เคธ + เคฎเคฟเคถเฅเคฐเคฟเคค เคŸเฅ‡เคชเคธ + เคฎเคฟเคถเฅเคฐเคฟเคค เคŸเฅ‡เคช + เคฐเฅ€เคฎเคฟเค•เฅเคธ + เคฐเคนเคจเคพ + เคชเฅเคฒเฅ‡เคฒเคฟเคธเฅเคŸ เค•เคพ เคจเคพเคฎ เคฌเคฆเคฒเฅ‡เค‚ + เคนเคŸเคพเคเค + เคธเค‚เคชเคพเคฆเคจ เค•เคฐเฅ‡เค‚ + เคตเฅเคฏเคตเคนเคพเคฐ + + %d เค•เคฒเคพเค•เคพเคฐ + %d เค•เคฒเคพเค•เคพเคฐ + + เคธเค‚เค—เฅ€เคค เคฒเคพเค‡เคฌเฅเคฐเฅ‡เคฐเฅ€ เค•เฅ€ เคจเคฟเค—เคฐเคพเคจเฅ€ + เคœเฅ‹เคกเคผเฅ‡เค‚ + เคˆเคชเฅ€ + เคจเคพเคฎ เคฌเคฆเคฒเฅ‡เค‚ + เค…เคฐเฅเคงเคตเคฟเคฐเคพเคฎ (;) + เคกเฅ€เคœเฅ‡ เคฎเคฟเค•เฅเคธ + เคกเฅ€เคœเฅ‡ เคฎเคฟเค•เฅเคธ + เคชเฅเคฒเฅ‡เคฒเคฟเคธเฅเคŸ เคนเคŸเคพ เคฆเฅ€ เค—เคˆ + เคฆเคฟเคจเคพเค‚เค• + เค…เคตเคงเคฟ + เค—เฅ€เคคเฅ‹เค‚ เค•เฅ€ เค—เคฟเคจเคคเฅ€ + เคกเคฟเคธเฅเค• + เคŸเฅเคฐเฅˆเค• + เคกเคฟเคธเฅเค• %d + เคธเฅˆเค‚เคชเคฒ เคฐเฅ‡เคŸ + เค—เฅเคฃเคงเคฐเฅเคฎ เคฆเฅ‡เค–เฅ‡เค‚ + เค—เฅ€เคค เค•เฅ‡ เค—เฅเคฃเคงเคฐเฅเคฎ + เคกเคฟเคธเฅเคชเคฒเฅ‡ + เค•เคธเฅเคŸเคฎ เคจเฅ‹เคŸเฅ€เคซเคฟเค•เฅ‡เคถเคจ เคเค•เฅเคถเคจ + เค•เคฒเคพเค•เคพเคฐ เคธเฅ‡ เคšเคฒเคพเคเค‚ + เคถเฅˆเคฒเฅ€ เคธเฅ‡ เคšเคฒเคพเคเค‚ + เคซเฅ‡เคฐเคฌเคฆเคฒ เคฏเคพเคฆ เคฐเค–เฅ‡เค‚ + เคจเคฏเคพ เค—เคพเคจเคพ เคฌเคœเคพเคคเฅ‡ เคธเคฎเคฏ เคซเฅ‡เคฐเคฌเคฆเคฒ เค•เคฐเคคเฅ‡ เคฐเคนเฅ‡เค‚ + เคฎเคฒเฅเคŸเฅ€-เคฎเฅ‚เคฒเฅเคฏ เคตเคฟเคญเคพเคœเค• + เคฒเฅ‹เคก เค•เฅ€ เค—เคˆ เคถเฅˆเคฒเคฟเคฏเคพเค: %d + เคฒเฅ‹เคก เค•เคฟเค เค—เค เคเคฒเฅเคฌเคฎ: %d + เค†เคชเค•เฅ€ เคธเค‚เค—เฅ€เคค เคฒเคพเค‡เคฌเฅเคฐเฅ‡เคฐเฅ€ เคฒเฅ‹เคก เค•เคฐ เคฐเคนเฅ‡ เคนเฅˆเค‚... (%1$d/%2$d) + %d kbps + เค†เคชเค•เฅ€ เคธเค‚เค—เฅ€เคค เคฒเคพเค‡เคฌเฅเคฐเฅ‡เคฐเฅ€ เคฒเฅ‹เคก เค•เคฐ เคฐเคนเฅ‡ เคนเฅˆเค‚โ€ฆ + เคชเฅเคฒเฅ‡เคฒเคฟเคธเฅเคŸ เคฌเคจเคพเคˆ เค—เคˆ + เคฆเคฟเค–เคพเคˆ เคฆเฅ‡เคคเคพ เคนเฅˆ + เคธเคพเคเคพ เค•เคฐเฅ‡เค‚ + เคถเคซเคฒ เค•เคฐเฅ‡เค‚ + เคธเฅเคฅเคฟเคคเคฟ เคฌเคนเคพเคฒ + เคชเฅเคฒเฅ‡เคฒเคฟเคธเฅเคŸ เค•เคพ เคจเคพเคฎ เคฌเคฆเคฒเคพ เค—เคฏเคพ + เค…เคฒเฅ‡เค•เฅเคœเฅ‡เค‚เคกเคฐ เค•เฅˆเคชเคนเคพเคฐเฅเคŸ เคฆเฅเคตเคพเคฐเคพ เคตเคฟเค•เคธเคฟเคค + เคเค•เคพเคงเคฟเค• เคŸเฅˆเค— เคฎเคพเคจเฅ‹เค‚ เค•เฅ‹ เคจเคฟเคฐเฅ‚เคชเคฟเคค เค•เคฐเคจเฅ‡ เคตเคพเคฒเฅ‡ เคตเคฐเฅเคฃ เค•เฅ‰เคจเฅเคซเคผเคฟเค—เคฐ เค•เคฐเฅ‡เค‚ + เค…เคฒเฅเคชเคตเคฟเคฐเคพเคฎ (,) + เคธเฅเคฒเฅˆเคถ (/) + -%.1f dB + เคธเค‚เคชเคพเคฆเคจ %s \ No newline at end of file diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 783f6f805..7382cab1d 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -149,10 +149,10 @@ %d kbps %d Hz Uฤitavanje tvoje zbirke blazbe โ€ฆ (%1$d/%2$d) - Uฤitano pjesama: %d - Uฤitano albuma: %d - Uฤitanih izvoฤ‘aฤa: %d - Uฤitano ลพanrova: %d + Broj uฤitanih pjesama: %d + Broj uฤitanih albuma: %d + Broj uฤitanih izvoฤ‘aฤa: %d + Broj uฤitanih ลพanrova: %d Ukupno trajanje: %s %d pjesma @@ -215,13 +215,13 @@ Ampersand (&) Kompilacija uลพivo Kompilacija remiksa - Kompilacije + DJ kompilacije Znakovi odjeljivanja vrijednosti Prekini reprodukciju Konfiguriraj znakove koji oznaฤavaju viลกestruke vrijednosti oznaka Kosa crta (/) Plus (+) - Kompilacija + DJ kompilacija Toฤka-zarez (;) Prilagoฤ‘ena radnja trake reprodukcije Ekvilajzer @@ -286,4 +286,8 @@ Dodano u popis pjesama Uredi Izbrisati %s\? To je nepovratna radnja. + Ureฤ‘ivanje popisa pjesama %s + Sudjelovanja: + Dijeli + Nema diska \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 964385c34..fb003c4c8 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -8,7 +8,7 @@ Permetti Generi Artisti - Dischi + Album Canzoni Tutte le canzoni Cerca @@ -17,21 +17,21 @@ Ordine Nome Artista - Disco + Album Anno Ascendente Ora in riproduzione Riproduci Mescola Riproduci da tutte le canzoni - Riproduci dal disco + Riproduci dall\'album Riproduci dall\'artista Coda Riproduci successivo Accoda Accodato Vai all\'artista - Vai al disco + Vai all\'album Stato salvato Aggiungi Salva @@ -100,8 +100,8 @@ Cancella la query di ricerca Rimuovi cartella Icona Auxio - Copertina disco - Copertina disco per %s + Copertina album + Copertina album per %s Immagine artista per %s Immagine genere per %s @@ -129,7 +129,7 @@ Grigio Canzoni trovate: %d - Dischi trovati: %d + Album trovati: %d Artisti trovati: %d Generi trovati: %d Durata totale: %s @@ -139,9 +139,9 @@ %d canzoni - %d disco - %d dischi - %d dischi + %d album + %d album + %d album Modo La musica non sarร  caricata dalle cartelle che aggiungi. @@ -169,7 +169,7 @@ Free Lossless Audio Codec (FLAC) Advanced Audio Coding (AAC) Disco %d - %d kbps + %d kB/s -%.1f dB Caricamento musica Caricamento libreria musicaleโ€ฆ @@ -240,8 +240,8 @@ E commerciale (&) Raccolte live Raccolta di remix - Mixes - Mix + Mix DJ + Mix DJ Alta qualitร  Virgola (,) Punto e virgola (;) @@ -257,7 +257,7 @@ Impossibile svuotare Mescola selezionati Riproduci selezionati - %d Selezionati + %d selezionati Riproduci dal genere Wiki %1$s, %2$s @@ -295,4 +295,8 @@ Eliminare %s\? L\'operazione non puรฒ essere annullata. Playlist eliminata Playlist rinominata + Condividi + Nessun disco + Appare su + Modifica di %s \ 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 263a5a955..2c89cd98d 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -9,7 +9,7 @@ Artiesten Albums Nummers - Alle Nummers + Alle nummers Zoeken Filter Alles @@ -20,7 +20,7 @@ Speel van alle nummers Speel af van album Speel van artiest - Afspeelscherm + Nu afspelen Wachtrij Afspelen als volgende Toevoegen aan wachtrij @@ -30,15 +30,15 @@ Staat gered Toevoegen Opslaan - Geen Mappen + Geen mappen Over Versie - Bekijken op GitHub + Broncode Licenties - Ontwikkeld door OxygenCobalt + Ontwikkeld door Alexander Capehart Instellingen - Uiterlijk + Uiterlijk en gevoel Thema Automatisch Licht @@ -60,36 +60,36 @@ Geen muziek aangetroffen Laden van muziek mislukt Auxio heeft toestemming nodig om uw muziekbibliotheek te lezen - Geen app kan deze link openen + Geen app gevonden die deze taak kan uitvoeren Deze map wordt niet ondersteund Zoek in uw bibliotheekโ€ฆ Nummer %d - Afspelen/Pauzeren + Afspelen of pauzeren Naar volgend nummer gaan Naar het laatste nummer gaan Herhaalfunctie wijzigen Zoekopdracht wissen - Verwijder uitgesloten map + Map verwijderen Auxio pictogram - Artist Image voor %s - Artist Image voor %s - Genre Image voor %s + Albumhoes voor %s + Artiesten-afbeelding voor %s + Genre-afbeelding voor %s - Onbekend Genre - Geen datum + Onbekend genre + Geen datum Rood Roze Paars - Donkerpaars + Dieppaars Indigoblauw Blauw - Donkerblauw + Diepblauw Blauwgroen Groen - Donkergroen + Diepgroen Cyaan Geelgroen Geel @@ -99,14 +99,14 @@ Nummers geladen: %d - %d Nummer - %d Nummers + %d lied + %d liedjes - %d Album - %d Albums + %d album + %d albums - Onbekend Artist + Onbekende artiest Zwart thema Gebruik een puur-zwart donker thema Pauze op herhaling @@ -118,7 +118,7 @@ Bekijk eigenschappen Naam Artiest - @android:string/cancel + Annuleren Bibliotheek tabbladen Jaar Ouderpad @@ -133,25 +133,25 @@ Modus Bepaal waar muziek vandaan moet worden geladen Totale duur: %s - Shuffle Alles - @android:string/ok + Alles schudden + Okรฉ Altijd beginnen met spelen als een headset is aangesloten (werkt mogelijk niet op alle apparaten) Schakel shuffle aan of uit - Kan afspeelstatus wissen + De muziekbibliotheek opnieuw laden, indien mogelijk met behulp van tags uit het cachegeheugen Uw muziekbibliotheek wordt geladenโ€ฆ (%1$d/%2$d) Uitgezonderd Alle liedjes shuffelen Includeer Pauze wanneer een liedje wordt herhaald Muziek zal niet worden geladen vanuit de mappen die u toevoegt. - Muziek herladen + Muziek verfrissen Muziek zal alleen worden geladen uit de mappen die u toevoegt. Aanpassing met tags Aanpassing zonder tags Er speelt geen muziek Bij het afspelen van item details Ronde modus - Afgeronde hoeken op extra UI-elementen inschakelen (vereist dat albumhoezen afgerond zijn) + Afgeronde hoeken inschakelen voor extra UI-elementen (vereist dat albumhoezen zijn afgerond) Staat gerestaureerd Bibliotheekstatistieken Verander de zichtbaarheid en volgorde van bibliotheek-tabbladen @@ -161,18 +161,18 @@ Afspelen vanaf getoond item Afspeelstatus herstellen Herstel de eerder opgeslagen afspeelstatus (indien aanwezig) - Geen staat kan hersteld worden + Kan status niet herstellen Verwijder dit wachtrij liedje Verplaats dit wachtrij liedje Verplaats deze tab Album cover - Geen tracknummer + Geen nummer -%.1f dB Dynamisch - MPEG-1 Audio - MPEG-4 Audio - Ogg Audio - Matroska Audio + MPEG-1 audio + MPEG-4-audio + Ogg audio + Matroska-audio Albums geladen: %d Artiesten geladen: %d Genres geladen: %d @@ -189,4 +189,111 @@ Shuffle Geavanceerde audio codering (GAC) Gratis verliesvrije audiocodec (GVAC) + Nieuwe afspeellijst + Afspeelstatus gewist + Geluids- en afspeelgedrag configureren + Een nieuwe afspeellijst maken + %d Geselecteerd + Toevoegen aan afspeellijst + Afspeellijst gemaakt + Toegevoegd aan afspeellijst + Het thema en de kleuren van de app wijzigen + UI-besturingselementen en gedrag aanpassen + Bepaal hoe muziek en afbeeldingen worden geladen + Muziek + Automatisch herladen + Puntkomma (;) + Komma (,) + Plus (+) + Waarschuwing: Het gebruik van deze instelling kan ertoe leiden dat sommige tags verkeerd geรฏnterpreteerd worden als tags met meerdere waarden. U kunt dit oplossen door ongewenste scheidingstekens vooraf te laten gaan door een backslash (\\). + Toon alleen artiesten die rechtstreeks op een album worden genoemd (werkt het beste op goed getagde bibliotheken) + Sorteer namen die beginnen met cijfers of woorden zoals \"de\" correct (werkt het beste met Engelstalige muziek) + Stop met afspelen + Geselecteerd afspelen + Uw muziekbibliotheek wordt geladenโ€ฆ + Gedrag + Remix compilatie + Soundtrack + Mixtape + DJ-mix + Remixen + Schuine streep (/) + WeergaveWinst + Volharding + Afspeellijst + Wiki + Kan status niet opslaan + Resetten + Afbeeldingen + Afspeelstatus wissen + Snel + Bibliotheek + Live EP + Remix EP + Live single + Remix single + Compilaties + Compilatie + Live + Afspeellijst hernoemen + Afspeellijst verwijderen\? + Herhaalmodus + Mappen + De wachtrij openen + %s verwijderen\? Dit kan niet ongedaan worden gemaakt. + Uit + Hoge kwaliteit + Genre + Ampersand (&) + Bewerken + Aflopend + Kan status niet wissen + Afspeellijst-afbeelding voor %s + Geen nummers + Gelijkmaker + Singles + Single + EP\'s + EP + Live album + Remix album + Soundtracks + Mixtapes + Muziek laden + Muziekbibliotheek bewaken + Live compilatie + DJ-mixen + Uw muziekbibliotheek controleren op wijzigingenโ€ฆ + Afspeellijst hernoemd + Hernoemen + Aangepaste afspeelbalkactie + Tekens configureren die meerdere tagwaarden aanduiden + Verwijderen + Scheiders met meerdere waarden + Verberg bijdragers + Speel vanuit genre + Datum toegevoegd + %1$s, %2$s + Afspeellijst %d + + %d artiest + %d artiesten + + Shuffle geselecteerd + Intelligent sorteren + Verschijnt op + Afspeellijsten + Delen + Afspeellijst verwijderd + Naar volgende + Laad de muziekbibliotheek opnieuw wanneer deze wordt gewijzigd (vereist permanente melding) + Niet-muziek uitsluiten + Negeer audiobestanden die geen muziek zijn, zoals podcasts + Albumhoezen + Afspeel + Muziek opnieuw scannen + De tag-cache wissen en de muziekbibliotheek volledig opnieuw laden (langzamer, maar vollediger) + De eerder opgeslagen afspeelstatus wissen (indien aanwezig) + Geen schijf + %s aan het bewerken \ No newline at end of file diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index 7e70b3cda..f51c29ee1 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -27,8 +27,8 @@ เจธเจพเจŠเจ‚เจกเจŸเฉเจฐเฉˆเจ• เจธเจพเจŠเจ‚เจกเจŸเฉเจฐเฉˆเจ•เจธ เจฎเจฟเจ•เจธเจŸเฉ‡เจชเจธ - เจฎเจฟเจ•เจธ - เจฎเจฟเจ•เจธ + เจกเฉ€เจœเฉ‡ เจฎเจฟเจ•เจธ + เจกเฉ€เจœเฉ‡ เจฎเจฟเจ•เจธ เจฒเจพเจˆเจต เจฐเฉ€เจฎเจฟเจ•เจธ เจ•เจฒเจพเจ•เจพเจฐ @@ -180,7 +180,7 @@ เจ•เฉŒเจฎเจพ (,) เจธเฉˆเจฎเฉ€เจ•เฉ‹เจฒเจจ (;) เจธเจฒเฉˆเจธเจผ (/) - เจเจ‚เจชเจฐเจธเฉˆเจ‚เจก (&) + Ampersand (&) เจธเจนเจฟเจฏเฉ‹เจ—เฉ€เจ†เจ‚ เจจเฉ‚เฉฐ เจฒเฉเจ•เจพเจ“ เจ†เจตเจพเจœเจผ เจ…เจคเฉ‡ เจชเจฒเฉ‡เจฌเฉˆเจ• เจตเจฟเจตเจนเจพเจฐ เจฆเฉ€ เจธเฉฐเจฐเจšเจจเจพ เจ•เจฐเฉ‹ เจชเจฒเฉ‡เจ…เจฌเฉˆเจ• @@ -202,4 +202,89 @@ เจธเจผเจซเจฒ เจšเจพเจฒเฉ‚ เจœเจพเจ‚ เจฌเฉฐเจฆ เจ•เจฐเฉ‹ เจธเจพเจฐเฉ‡ เจ—เฉ€เจคเจพเจ‚ เจจเฉ‚เฉฐ เจธเจผเจซเจฒ เจ•เจฐเฉ‹ เจชเจฒเฉ‡เจฌเฉˆเจ• เจฌเฉฐเจฆ เจ•เจฐเฉ‹ + เจจเจตเฉ€เจ‚ เจชเจฒเฉ‡เจ…-เจฒเจฟเจธเจŸ + เจชเจฒเฉ‡เจ…-เจฒเจฟเจธเจŸ เจตเจฟเฉฑเจš เจœเฉ‹เฉœเฉเจนเฉ‹ + %1$s, %2$s + เจซเจฟเฉฑเจ•เจพ เจจเฉ€เจฒเจพ-เจนเจฐเจพ + เจชเจฒเฉ‡เจ…-เจฒเจฟเจธเจŸ + เจชเจฒเฉ‡เจ…-เจฒเจฟเจธเจŸ เจฎเจฟเจŸเจพเจˆ + เจชเจฒเฉ‡เจ…-เจฒเจฟเจธเจŸ เจฌเจฃ เจ—เจˆ + เจชเจฒเฉ‡เจ…-เจฒเจฟเจธเจŸ เจฆเจพ เจจเจพเจ‚ เจฌเจฆเจฒเจฟเจ† + เจจเฉ€เจฒเจพ-เจนเจฐเจพ + เจฎเจฟเจŸเจพเจ“ + เจ‡เฉฑเจ• เจจเจตเฉ€เจ‚ เจชเจฒเฉ‡เจ…-เจฒเจฟเจธเจŸ เจฌเจฃเจพเจ“ + เจชเจฒเฉ‡เจ…- เจฒเจฟเจธเจŸ เจตเจฟเฉฑเจš เจธเจผเจพเจฎเจฟเจฒ เจ•เฉ€เจคเจพ + เจ‡เจน เจŸเฉˆเจฌ เจนเจฟเจฒเจพเจ“ + เจ•เฉ‹เจˆ เจ—เฉ€เจค เจจเจนเฉ€เจ‚ + Matroska เจ†เจกเฉ€เจ“ + เจ—เฉ‚เฉœเฉเจนเจพ เจœเฉเจนเจพเจฎเจฃเฉ€ + Ogg เจ†เจกเฉ€เจ“ + % d: เจ—เฉ€เจค เจฒเฉ‹เจก เจ•เฉ€เจคเฉ‡ + + %d เจเจฒเจฌเจฎ + %d เจเจฒเจฌเจฎเจพเจ‚ + + เจจเจพเจ‚ เจฌเจฆเจฒเฉ‹ + เจชเจฒเฉ‡เจ…-เจฒเจฟเจธเจŸ เจฆเจพ เจจเจพเจ‚ เจฌเจฆเจฒเฉ‹ + เจชเจฒเฉ‡เจ…-เจฒเจฟเจธเจŸ เจฎเจฟเจŸเจพเจ“\? + เจธเฉ‚เจ-เจฌเฉ‚เจ เจจเจพเจฒ เจฒเฉœเฉ€เจฌเฉฑเจง + เจ‰เจนเจจเจพเจ‚ เจจเจพเจตเจพเจ‚ เจจเฉ‚เฉฐ เจ เฉ€เจ• เจคเจฐเฉเจนเจพเจ‚ เจฒเฉœเฉ€เจฌเฉฑเจง เจ•เจฐเฉ‹ เจœเฉ‹ เจจเฉฐเจฌเจฐเจพเจ‚ เจœเจพเจ‚ เจธเจผเจฌเจฆเจพเจ‚ เจจเจพเจฒ เจธเจผเฉเจฐเฉ‚ เจนเฉเฉฐเจฆเฉ‡ เจนเจจ เจœเจฟเจตเฉ‡เจ‚ เจ•เจฟ \"เจฆเจพ\" (เจ…เฉฐเจ—เจฐเฉ‡เจœเจผเฉ€-เจญเจพเจธเจผเจพ เจฆเฉ‡ เจธเฉฐเจ—เฉ€เจค เจฆเฉ‡ เจจเจพเจฒ เจธเจญ เจคเฉ‹เจ‚ เจตเจงเฉ€เจ† เจ•เฉฐเจฎ เจ•เจฐเจฆเจพ เจนเฉˆ) + %s เจนเจŸเจพเจ‰เจฃเจพ เจนเฉˆ\? เจ‡เจธ เจจเฉ‚เฉฐ เจ…เจฃเจ•เฉ€เจคเจพ เจจเจนเฉ€เจ‚ เจ•เฉ€เจคเจพ เจœเจพ เจธเจ•เจฆเจพเฅค + เจ—เฉ‚เฉœเฉเจนเจพ เจจเฉ€เจฒเจพ + + %d เจ•เจฒเจพเจ•เจพเจฐ + %d เจ•เจฒเจพเจ•เจพเจฐ + + เจกเจฟเจธเจ• %d + เจจเฉ€เจฒเจพ + เจชเจฒเฉ‡เจ…-เจฒเจฟเจธเจŸ %d + MPEG-1 เจ†เจกเฉ€เจ“ + MPEG-4 เจ†เจกเฉ€เจ“ + %d เจšเฉเจฃเฉ‡ + เจนเจฐเจพ + เจ–เฉ‹เจœ เจ•เจฐเฉ€ เจธเจพเจซเจผ เจ•เจฐเฉ‹ + เจ•เฉ‹เจˆ เจŸเจฐเฉˆเจ• เจจเจนเฉ€เจ‚ + เจ—เฉเจฒเจพเจฌเฉ€ + เจœเฉเจนเจพเจฎเจฃเฉ€ + เจ…เจกเจตเจพเจ‚เจธเจก เจ†เจกเฉ€เจ“ เจ•เฉ‹เจกเจฟเฉฐเจ— (AAC) + เจซเจฐเฉ€ เจฒเฉ‚เจœเจผเจฒเฉˆเฉฑเจธ เจ†เจกเฉ€เจ“ Codec (FLAC) + เจฒเจพเจฒ + เจธเฉฐเจคเจฐเฉ€ + %d: เจถเฉˆเจฒเฉ€เจ†เจ‚ เจฒเฉ‹เจก เจ•เฉ€เจคเฉ€เจ†เจ‚ + %d: เจเจฒเจฌเจฎ เจฒเฉ‹เจก เจ•เฉ€เจคเฉ‡ + %d: เจ•เจฒเจพเจ•เจพเจฐ เจฒเฉ‹เจก เจ•เฉ€เจคเฉ‡ + เจธเฉ‹เจง เจ•เจฐเฉ‹ + %s เจฒเจˆ เจเจฒเจฌเจฎ เจ•เจตเจฐ + เจเจฒเจฌเจฎ เจ•เจตเจฐ + %s เจฒเจˆ เจ•เจฒเจพเจ•เจพเจฐ เจšเจฟเฉฑเจคเจฐ + %s เจฒเจˆ เจธเจผเฉˆเจฒเฉ€ เจšเจฟเฉฑเจคเจฐ + %s เจฒเจˆ เจชเจฒเฉ‡เจ…-เจฒเจฟเจธเจŸ เจšเจฟเฉฑเจคเจฐ + เจ…เจฃเจœเจพเจฃ เจ•เจฒเจพเจ•เจพเจฐ + เจ…เจฃเจœเจพเจฃ เจธเจผเฉˆเจฒเฉ€ + เจ•เฉ‹เจˆ เจฎเจฟเจคเฉ€ เจจเจนเฉ€เจ‚ + เจ•เฉ‹เจˆ เจธเฉฐเจ—เฉ€เจค เจจเจนเฉ€เจ‚ เจšเฉฑเจฒ เจฐเจฟเจนเจพ + เจ—เฉ‚เฉœเฉเจนเจพ เจนเจฐเจพ + เจชเฉ€เจฒเฉเจนเจพ + เจจเจฟเฉฐเจฌเฉ‚ เจฐเฉฐเจ—เจพ + +%.1f dB + %d kbps + %d Hz + เจญเฉ‚เจฐเจพ + -%.1f dB + + %d เจ—เฉ€เจค + %d เจ—เฉ€เจค + + เจธเจฒเฉ‡เจŸเฉ€ + เจ•เฉเฉฑเจฒ เจฎเจฟเจ†เจฆ: %s + เจซเฉ‹เจฒเจกเจฐ เจนเจŸเจพเจ“ + Auxio เจ†เจˆเจ•เจพเจจ + เจ‰เฉฑเจคเฉ‡ เจตเจฟเจ–เจพเจˆ เจฆเจฟเฉฐเจฆเจพ เจนเฉˆ + เจชเจฒเฉ‡เจ…-เจฒเจฟเจธเจŸเจพเจ‚ + เจธเจพเจ‚เจเจพ เจ•เจฐเฉ‹ + เจ•เฉ‹เจˆ เจกเจฟเจธเจ• เจจเจนเฉ€เจ‚ + เจฌเฉˆเจ‚เจ—เจฃเฉ€เจ‚ + เจกเจพเจ‡เจจเฉˆเจฎเจฟเจ• + %s เจธเฉ‹เจง เจฐเจฟเจนเจพ + เจคเฉเจนเจพเจกเฉ€ เจธเฉฐเจ—เฉ€เจค เจฒเจพเจ‡เจฌเจฐเฉ‡เจฐเฉ€ เจฒเฉ‹เจก เจ•เฉ€เจคเฉ€ เจœเจพ เจฐเจนเฉ€ เจนเฉˆโ€ฆ (%1$d/%2$d) \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 26f8b19bc..6091d3ea4 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -191,7 +191,7 @@ ะšะพะฝั†ะตั€ั‚ะฝั‹ะน ะฐะปัŒะฑะพะผ ะšะพะฝั†ะตั€ั‚ะฝั‹ะน ะœะพะฝะธั‚ะพั€ะธะฝะณ ะธะทะผะตะฝะตะฝะธะน ะฒ ะผัƒะทั‹ะบะฐะปัŒะฝะพะน ะฑะธะฑะปะธะพั‚ะตะบะตโ€ฆ - ะŸะพะทะธั†ะธั ะพั‡ะธั‰ะตะฝะฐ + ะŸะพะทะธั†ะธั ัะฑั€ะพัˆะตะฝะฐ ะŸะฐะฟะบะธ ั ะผัƒะทั‹ะบะพะน ะ’ะบะปัŽั‡ะธั‚ัŒ ะะปัŒะฑะพะผ ั€ะตะผะธะบัะพะฒ diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index db0b15c59..99d462df2 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -275,4 +275,9 @@ Sฤฑralama yaparken makaleleri yoksay Ada gรถre sฤฑralarken \"the\" gibi kelimeleri yok sayฤฑn (en iyi ingilizce mรผzikle รงalฤฑลŸฤฑr) Yeni bir oynatma listesi oluลŸtur + Yeni Oynatma Listesi + Sil + Yeniden Adlandฤฑr + Oynatma Listesini Yeniden Adlandฤฑr + Oynatma listesini silmek istiyor musun\? \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 038e65384..a9a5c1e6a 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -27,7 +27,7 @@ ะ›ั–ั†ะตะฝะทั–ั— ะะฐะปะฐัˆั‚ัƒะฒะฐะฝะฝั - ะ’ะธะณะปัะด ั– ะฟะพะฒะตะดั–ะฝะบะฐ + ะ’ะธะณะปัะด ะขะตะผะฐ ะกะฒั–ั‚ะปะฐ ะขะตะผะฝะฐ diff --git a/fastlane/metadata/android/fi/short_description.txt b/fastlane/metadata/android/fi/short_description.txt new file mode 100644 index 000000000..f145fe7da --- /dev/null +++ b/fastlane/metadata/android/fi/short_description.txt @@ -0,0 +1 @@ +Yksinkertainen ja rationaallinen musiikkisoitin diff --git a/fastlane/metadata/android/hi/full_description.txt b/fastlane/metadata/android/hi/full_description.txt index 2b9dea82a..b22a5b7c4 100644 --- a/fastlane/metadata/android/hi/full_description.txt +++ b/fastlane/metadata/android/hi/full_description.txt @@ -10,7 +10,8 @@ Auxio เคเค• เคคเฅ‡เคœเคผ, เคตเคฟเคถเฅเคตเคธเคจเฅ€เคฏ UI/UX เคตเคพเคฒเคพ เคเค• - เคกเคฟเคธเฅเค• เคธเค‚เค–เฅเคฏเคพ, เคเค•เคพเคงเคฟเค• เค•เคฒเคพเค•เคพเคฐ, เคฐเคฟเคฒเฅ€เคœเคผ เคชเฅเคฐเค•เคพเคฐ, เคธเคŸเฅ€เค• เค•เฅ‡ เคฒเคฟเค เคธเคฎเคฐเฅเคฅเคจ / เคฎเฅ‚เคฒ เคฆเคฟเคจเคพเค‚เค•, เคธเฅ‰เคฐเฅเคŸ เคŸเฅˆเค—, เค”เคฐ เคฌเคนเฅเคค เค•เฅเค› - เค‰เคจเฅเคจเคค เค•เคฒเคพเค•เคพเคฐ เคชเฅเคฐเคฃเคพเคฒเฅ€ เคœเฅ‹ เค•เคฒเคพเค•เคพเคฐเฅ‹เค‚ เค”เคฐ เคเคฒเฅเคฌเคฎ เค•เคฒเคพเค•เคพเคฐเฅ‹เค‚ เค•เฅ‹ เคเค•เคœเฅเคŸ เค•เคฐเคคเฅ€ เคนเฅˆ - เคเคธเคกเฅ€ เค•เคพเคฐเฅเคก-เคœเคพเค—เคฐเฅ‚เค• เคซเคผเฅ‹เคฒเฅเคกเคฐ เคชเฅเคฐเคฌเค‚เคงเคจ -- เคตเคฟเคถเฅเคตเคธเคจเฅ€เคฏ เคชเฅเคฒเฅ‡เคฌเฅˆเค• เคธเฅเคฅเคฟเคคเคฟ เคฆเฅƒเคขเคผเคคเคพ +- เคตเคฟเคถเฅเคตเคธเคจเฅ€เคฏ เคชเฅเคฒเฅ‡เคฒเคฟเคธเฅเคŸเคฟเค‚เค— เค•เคพเคฐเฅเคฏเค•เฅเคทเคฎเคคเคพ +- เคชเฅเคฒเฅ‡เคฌเฅˆเค• เค…เคตเคธเฅเคฅเคพ เคฆเฅƒเคขเคผเคคเคพ - เคชเฅ‚เคฐเฅเคฃ เคฐเฅ€เคชเฅเคฒเฅ‡เค—เฅˆเคจ เคธเคฎเคฐเฅเคฅเคจ (MP3, FLAC, OGG, OPUS เค”เคฐ MP4 เคซเคผเคพเค‡เคฒเฅ‹เค‚ เคชเคฐ) - เคฌเคพเคนเคฐเฅ€ เคคเฅเคฒเฅเคฏเค•เคพเคฐเค• เคธเคฎเคฐเฅเคฅเคจ (เค‰เคฆเคพเฅค เคตเฅ‡เคตเคฒเฅ‡เคŸ) - เคเคœ-เคŸเฅ‚-เคเคœ diff --git a/fastlane/metadata/android/hr/full_description.txt b/fastlane/metadata/android/hr/full_description.txt index ee782d658..3973c6e3a 100644 --- a/fastlane/metadata/android/hr/full_description.txt +++ b/fastlane/metadata/android/hr/full_description.txt @@ -1,22 +1,23 @@ -Auxio je lokalni izvoฤ‘aฤ glazbe s brzim i pouzdanim korisniฤkim suฤeljem/korisniฤkim iskustvom bez nepotrebnih znaฤajki koje su prisutne u ostalim izvoฤ‘aฤima glazbe. Kreiran od Exoplayera, Auxio ima vrhunsku podrลกku za biblioteke i kvalitetu sluลกanja u usporedbi s drugim aplikacijama koje koriste zastarjelu funkcionalnost Androida. Ukratko, reproducira glazbu. +Auxio je lokalni glazbeni player s brzim, pouzdanim UI/UX bez mnogih beskorisnih znaฤajki prisutnih u drugim glazbenim playerima. Izgraฤ‘en na temelju ExoPlayera, Auxio ima vrhunsku podrลกku za biblioteku i kvalitetu sluลกanja u usporedbi s drugim aplikacijama koje koriste zastarjelu funkcionalnost Androida. Ukratko, Reproducira glazbu. Znaฤajke -- Reprodukcija bazirana na ExoPlayeru -- Brzo korisniฤko suฤelje u skladu s najnovijim Materijal dizajnom -- Korisniฤko iskustvo koje priorizira jednostavnost koriลกtenja -- Prilagodljive ponaลกanje aplikacije -- Podrลกka za brojeve diskova, izvoฤ‘aฤe, vrste izdanja, -precizne/izvorne datume, oznake razvrstavanje i joลก viลกe +- Reprodukcija temeljena na ExoPlayeru +- Snappy UI izvedeno iz najnovijih smjernica za materijalni dizajn +- Iskustveni korisniฤki doลพivljaj koji daje prednost jednostavnosti upotrebe u odnosu na rubne sluฤajeve +- Prilagodljivo ponaลกanje +- Podrลกka za brojeve diskova, viลกe izvoฤ‘aฤa, vrste izdanja, +precizni/izvorni datumi, sortiranje oznaka i viลกe - Napredni sustav izvoฤ‘aฤa koji ujedinjuje izvoฤ‘aฤe i izvoฤ‘aฤe albuma -- Upravljanje mapama SD kartica -- Pouzdana postojanost stanja reprodukcije -- Potpuna ReplayGain podrลกka (Za MP3, MP4, FLAC, OGG, i OPUS formate) -- Podrลกka za eksterne ekvilajzere (npr. Wavelet) -- Prikaz od ruba do ruba -- Podrลกka za ugraฤ‘ene omote -- Pretraลพinje -- Moguฤ‡nost pokretanja glazbe ฤim spojite sluลกalice -- Stilizirani widgeti koji automatski prilagoฤ‘avaju svoju veliฤinu -- Potpuno privatan bez potrebe za internetskom vezom -- Bez zaobljenih omota albuma (Osim ako ih ลพelite. Onda ih moลพete imati.) +- Upravljanje mapama koje podrลพava SD karticu +- Pouzdana funkcija popisa pjesama +- Postojanost stanja reprodukcije +- Puna podrลกka za ReplayGain (na MP3, FLAC, OGG, OPUS i MP4 datotekama) +- Podrลกka za vanjski ekvilizator (npr. Wavelet) +- Od ruba do ruba +- Podrลกka za ugraฤ‘ene naslovnice +- Funkcionalnost pretraลพivanja +- Automatska reprodukcija sluลกalica +- Elegantni widgeti koji se automatski prilagoฤ‘avaju njihovoj veliฤini +- Potpuno privatno i izvan mreลพe +- Nema zaobljenih naslovnica albuma (Osim ako ih ne ลพelite. Onda moลพete.) diff --git a/fastlane/metadata/android/ko/full_description.txt b/fastlane/metadata/android/ko/full_description.txt index b6bf66379..0a1497368 100644 --- a/fastlane/metadata/android/ko/full_description.txt +++ b/fastlane/metadata/android/ko/full_description.txt @@ -6,12 +6,14 @@ Auxio๋Š” ๋‹ค๋ฅธ ์Œ์•… ํ”Œ๋ ˆ์ด์–ด์— ์กด์žฌํ•˜๋Š” ๋งŽ์€ ์“ธ๋ชจ์—†๋Š” ๊ธฐ๋Šฅ - ์ตœ์‹ ์ฃผ๋ชฉํ•  ๋งŒํ•œ ๋””์ž์ธ ๊ฐ€์ด๋“œ๋ผ์ธ์—์„œ ํŒŒ์ƒ๋œ Snappy UI - ์—ฃ์ง€ ์ผ€์ด์Šค๋ณด๋‹ค ์‚ฌ์šฉ ํŽธ์˜์„ฑ์„ ์šฐ์„ ์‹œํ•˜๋Š” ์˜๊ฒฌ์ด ๋งŽ์€ UX - ์‚ฌ์šฉ์ž ์ •์˜ ๊ฐ€๋Šฅํ•œ ๋™์ž‘ -- ์˜ฌ๋ฐ”๋ฅธ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์˜ ์šฐ์„  ์ˆœ์œ„๋ฅผ ์ง€์ •ํ•˜๋Š” ๊ณ ๊ธ‰ ๋ฏธ๋””์–ด ์ธ๋ฑ์„œ -- ์ •ํ™•ํ•œ/์›๋ž˜ ๋‚ ์งœ, ์ •๋ ฌ ํƒœ๊ทธ ๋ฐ ๋ฆด๋ฆฌ์Šค ์œ ํ˜• ์ง€์›(์‹คํ—˜์ ) +- ๋””์Šคํฌ ๋ฒˆํ˜ธ, ์—ฌ๋Ÿฌ ์•„ํ‹ฐ์ŠคํŠธ, ๋ฆด๋ฆฌ์Šค ์œ ํ˜• ์ง€์›, +์ •ํ™•ํ•œ/์›๋ณธ ๋‚ ์งœ, ์ •๋ ฌ ํƒœ๊ทธ ๋“ฑ ์ง€์› +- ์•„ํ‹ฐ์ŠคํŠธ์™€ ์•จ๋ฒ” ์•„ํ‹ฐ์ŠคํŠธ๋ฅผ ํ†ตํ•ฉํ•˜๋Š” ๊ณ ๊ธ‰ ์•„ํ‹ฐ์ŠคํŠธ ์‹œ์Šคํ…œ - SD ์นด๋“œ ์ธ์‹ ํด๋” ๊ด€๋ฆฌ -- ์•ˆ์ •์ ์ธ ์žฌ์ƒ ์ƒํƒœ ์ง€์†์„ฑ -- ์™„์ „ํ•œ ReplayGain ์ง€์› (MP3, MP4, FLAC, OGG, OPUS) -- ์™ธ๋ถ€ ์ดํ€„๋ผ์ด์ € ๊ธฐ๋Šฅ (Wavelet๊ณผ ๊ฐ™์€ ์•ฑ) +- ์•ˆ์ •์ ์ธ ์žฌ์ƒ ๋ชฉ๋ก ๊ธฐ๋Šฅ +- ์žฌ์ƒ ์ƒํƒœ ์ง€์†์„ฑ +- ์ „์ฒด ReplayGain ์ง€์› (MP3, FLAC, OGG, OPUS, MP4) +- ์™ธ๋ถ€ ์ดํ€„๋ผ์ด์ € ์ง€์› (์˜ˆ: Wavelet) - Edge-to-edge - ์ž„๋ฒ ๋””๋“œ ์ปค๋ฒ„ ์ง€์› - ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ diff --git a/fastlane/metadata/android/pa/full_description.txt b/fastlane/metadata/android/pa/full_description.txt index 1fc6c904d..d4b927d7b 100644 --- a/fastlane/metadata/android/pa/full_description.txt +++ b/fastlane/metadata/android/pa/full_description.txt @@ -8,7 +8,10 @@ Auxio เจ‡เฉฑเจ• เจคเฉ‡เจœเจผ, เจญเจฐเฉ‹เจธเฉ‡เจฎเฉฐเจฆ UI/UX เจตเจพเจฒเจพ เจ‡เฉฑ - เจ…เจจเฉเจ•เฉ‚เจฒเจฟเจค เจตเจฟเจตเจนเจพเจฐ - เจกเจฟเจธเจ• เจจเฉฐเจฌเจฐเจพเจ‚, เจฎเจฒเจŸเฉ€เจชเจฒ เจ•เจฒเจพเจ•เจพเจฐเจพเจ‚, เจฐเฉ€เจฒเฉ€เจœเจผ เจ•เจฟเจธเจฎเจพเจ‚, เจธเจŸเฉ€เจ• เจฒเจˆ เจธเจฎเจฐเจฅเจจ /เจฎเฉ‚เจฒ เจคเจพเจฐเฉ€เจ–เจพเจ‚, เจ•เฉเจฐเจฎเจฌเฉฑเจง เจŸเฉˆเจ—เจธ, เจ…เจคเฉ‡ เจนเฉ‹เจฐ - เจ‰เฉฑเจจเจค เจ•เจฒเจพเจ•เจพเจฐ เจชเฉเจฐเจฃเจพเจฒเฉ€ เจœเฉ‹ เจ•เจฒเจพเจ•เจพเจฐเจพเจ‚ เจ…เจคเฉ‡ เจเจฒเจฌเจฎ เจ•เจฒเจพเจ•เจพเจฐเจพเจ‚ เจจเฉ‚เฉฐ เจ‡เจ•เจœเฉเฉฑเจŸ เจ•เจฐเจฆเฉ€ เจนเฉˆ -- SD เจ•เจพเจฐเจก-เจœเจพเจฃเฉ‚ เจซเฉ‹เจฒเจกเจฐ เจชเฉเจฐเจฌเฉฐเจงเจจ - เจญเจฐเฉ‹เจธเฉ‡เจฏเฉ‹เจ— เจชเจฒเฉ‡เจฌเฉˆเจ• เจธเจฅเจฟเจคเฉ€ เจธเจฅเจฟเจฐเจคเจพ - เจชเฉ‚เจฐเจพ เจฐเฉ€เจชเจฒเฉ‡เจ—เฉ‡เจจ เจธเจฎเจฐเจฅเจจ (MP3, FLAC, OGG, OPUS, เจ…เจคเฉ‡ MP4 เจซเจพเจˆเจฒเจพเจ‚ 'เจคเฉ‡) +- SD เจ•เจพเจฐเจก-เจœเจพเจฃเฉ‚ เจซเฉ‹เจฒเจกเจฐ เจชเฉเจฐเจฌเฉฐเจงเจจ +- เจญเจฐเฉ‹เจธเฉ‡เจฏเฉ‹เจ— เจชเจฒเฉ‡เจ…เจฒเจฟเจธเจŸเจฟเฉฐเจ— เจ•เจพเจฐเจœเจ•เฉเจธเจผเจฒเจคเจพ +- เจญเจฐเฉ‹เจธเฉ‡เจฏเฉ‹เจ— เจชเจฒเฉ‡เจ…เจฌเฉˆเจ• เจธเจฅเจฟเจคเฉ€ เจธเจฅเจฟเจฐเจคเจพ +- เจชเฉ‚เจฐเจพ เจฐเฉ€เจชเจฒเฉ‡เจ—เฉ‡เจจ เจธเจฎเจฐเจฅเจจ (MP3, FLAC, OGG, OPUS, เจ…เจคเฉ‡ MP4 เจซเจพเจˆเจฒเจพเจ‚ 'เจคเฉ‡) - เจฌเจพเจนเจฐเฉ€ เจˆเจ•เฉ‹เจฒเจพเจˆเจœเจผเจฐ เจฆเจพ เจธเจฎเจฐเจฅเจจ (เจ‰เจฆเจพเจนเจฐเจจ. เจตเฉ‡เจตเจฒเฉ‡เจŸ) - เจ•เจฟเจจเจพเจฐเฉ‡-เจคเฉ‹เจ‚-เจ•เจฟเจจเจพเจฐเฉ‡ - เจเจฎเจฌเฉˆเจกเจก เจ•เจตเจฐ เจธเจชเฉ‹เจฐเจŸ From 839861b6b828a3c2110a8754dc979aaa2fdbb705 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 15 Jun 2023 11:53:24 -0600 Subject: [PATCH 24/26] music: fix grouping regressions Fix regressions in grouping, mostly around the kind of music that is prioritized and sort naming. --- CHANGELOG.md | 5 ++-- .../oxycblt/auxio/image/extractor/CoverUri.kt | 5 ++-- .../auxio/music/device/DeviceLibrary.kt | 29 ++++++++++++------- .../oxycblt/auxio/music/metadata/TagWorker.kt | 17 +++++++---- 4 files changed, 37 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa466a8bc..11e08ad80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,8 @@ ## dev #### What's Improved -- Tags formatted as `artistssort` or `albumartistssort` are now recognized by Auxio -- Non-english digit strings are now sorted better +- `artistssort`, `albumartistssort`, and `album_artists` tags are now recognized +- Non-english digit strings are sorted more correctly - Reduced visual loading time #### What's Fixed @@ -12,6 +12,7 @@ - Fixed selection not updating when playlists are changed - Fixed duplicate albums appearing in certain cases - Fixed ReplayGain adjustment not applying at the start of a song in certain cases +- Music cache is no longer migrated between devices ## 3.1.1 diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverUri.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverUri.kt index 4aadf932e..5e32d09ff 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverUri.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverUri.kt @@ -23,9 +23,10 @@ import android.net.Uri /** * Bundle of [Uri] information used in [CoverExtractor] to ensure consistent [Uri] use when loading * images. + * * @param mediaStore The album cover [Uri] obtained from MediaStore. - * @param song The [Uri] of the first song (by track) of the album, which can also be used to - * obtain an album cover. + * @param song The [Uri] of the first song (by track) of the album, which can also be used to obtain + * an album cover. * @author Alexander Capehart (OxygenCobalt) */ data class CoverUri(val mediaStore: Uri, val song: Uri) diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt index 5a70dc578..45034582d 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt @@ -123,6 +123,8 @@ class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: Mu val artistGrouping = mutableMapOf>() val genreGrouping = mutableMapOf>() + // TODO: Use comparators here + // All music information is grouped as it is indexed by other components. for (rawSong in rawSongs) { val song = SongImpl(rawSong, musicSettings) @@ -150,11 +152,13 @@ class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: Mu // Since albums are grouped fuzzily, we pick the song with the earliest track to // use for album information to ensure consistent metadata and UIDs. Fall back to // the name otherwise. - val trackLower = + val higherPriority = song.track != null && - (prioritized.track == null || song.track < prioritized.track) - val nameLower = song.name < prioritized.name - if (trackLower || nameLower) { + (prioritized.track == null || + song.track < prioritized.track || + (song.track == prioritized.track && song.name < prioritized.name)) + + if (higherPriority) { albumBody.raw = PrioritizedRaw(song.rawAlbum, song) } } else { @@ -187,8 +191,8 @@ class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: Mu // Genre information from higher songs in ascending alphabetical order are // prioritized. val prioritized = genreBody.raw.src - val nameLower = song.name < prioritized.name - if (nameLower) { + val higherPriority = song.name < prioritized.name + if (higherPriority) { genreBody.raw = PrioritizedRaw(rawGenre, song) } } else { @@ -217,11 +221,14 @@ class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: Mu // Album information from later dates is prioritized, as it is more // likely to contain the "modern" name of the artist if the information // really is in-consistent. Fall back to the name otherwise. - val dateEarlier = + val prioritize = album.dates != null && - (prioritized.dates == null || album.dates < prioritized.dates) - val nameLower = album.name < prioritized.name - if (dateEarlier || nameLower) { + (prioritized.dates == null || + album.dates > prioritized.dates || + (album.dates == prioritized.dates && + album.name < prioritized.name)) + + if (prioritize) { body.raw = PrioritizedRaw(rawArtist, album) } } @@ -243,6 +250,8 @@ class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: Mu } } +// TODO: Avoid redundant data creation + class DeviceLibraryImpl( override val songs: Set, override val albums: Set, diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt index 69569fb19..5d9eebcbc 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt @@ -141,7 +141,9 @@ private class TagWorkerImpl( textFrames["TALB"]?.let { rawSong.albumName = it.first() } textFrames["TSOA"]?.let { rawSong.albumSortName = it.first() } (textFrames["TXXX:musicbrainz album type"] - ?: textFrames["TXXX:releasetype"] ?: textFrames["GRP1"]) + ?: textFrames["TXXX:releasetype"] ?: + // This is a non-standard iTunes extension + textFrames["GRP1"]) ?.let { rawSong.releaseTypes = it } // Artist @@ -158,15 +160,17 @@ private class TagWorkerImpl( rawSong.albumArtistNames = it } (textFrames["TXXX:albumartistssort"] - ?: textFrames["TXXX:albumartists_sort"] ?: textFrames["TSO2"]) + ?: textFrames["TXXX:albumartists_sort"] ?: textFrames["TXXX:albumartistsort"] + // This is a non-standard iTunes extension + ?: textFrames["TSO2"]) ?.let { rawSong.albumArtistSortNames = it } // Genre textFrames["TCON"]?.let { rawSong.genreNames = it } // Compilation Flag - (textFrames["TCMP"] - ?: textFrames["TXXX:compilation"] ?: textFrames["TXXX:itunescompilation"]) + (textFrames["TCMP"] // This is a non-standard itunes extension + ?: textFrames["TXXX:compilation"] ?: textFrames["TXXX:itunescompilation"]) ?.let { // Ignore invalid instances of this tag if (it.size != 1 || it[0] != "1") return@let @@ -175,6 +179,7 @@ private class TagWorkerImpl( rawSong.albumArtistNames.ifEmpty { COMPILATION_ALBUM_ARTISTS } rawSong.releaseTypes = rawSong.releaseTypes.ifEmpty { COMPILATION_RELEASE_TYPES } } + // ReplayGain information textFrames["TXXX:replaygain_track_gain"]?.parseReplayGainAdjustment()?.let { rawSong.replayGainTrackAdjustment = it @@ -262,7 +267,9 @@ private class TagWorkerImpl( // Album artist comments["musicbrainz_albumartistid"]?.let { rawSong.albumArtistMusicBrainzIds = it } - (comments["albumartists"] ?: comments["albumartist"])?.let { rawSong.albumArtistNames = it } + (comments["albumartists"] ?: comments["album_artists"] ?: comments["albumartist"])?.let { + rawSong.albumArtistNames = it + } (comments["albumartistssort"] ?: comments["albumartists_sort"] ?: comments["albumartistsort"]) ?.let { rawSong.albumArtistSortNames = it } From 7b4af3515dee24294edfb034a389709e9ccca270 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 15 Jun 2023 19:11:31 -0600 Subject: [PATCH 25/26] music: prioritize earlier album artist info Prioritize artist information from earlier albums (date-wise), as it's less likely to change with the addition of new music. --- .../java/org/oxycblt/auxio/music/device/DeviceLibrary.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt index 45034582d..8032c46d7 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt @@ -218,13 +218,13 @@ class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: Mu // Immediately replace any songs that initially held the priority position. is SongImpl -> body.raw = PrioritizedRaw(rawArtist, album) is AlbumImpl -> { - // Album information from later dates is prioritized, as it is more - // likely to contain the "modern" name of the artist if the information - // really is in-consistent. Fall back to the name otherwise. + // Album artist information from earlier dates is prioritized, as it is + // less likely to change with the addition of new tracks. Fall back to + // the name otherwise. val prioritize = album.dates != null && (prioritized.dates == null || - album.dates > prioritized.dates || + album.dates < prioritized.dates || (album.dates == prioritized.dates && album.name < prioritized.name)) From c580d44466fd2267c7fb9f9b44e36481e12d3427 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 15 Jun 2023 19:33:30 -0600 Subject: [PATCH 26/26] build: bump to version 3.1.2 Bump the build version to 3.1.2 (32). --- CHANGELOG.md | 3 ++- README.md | 4 ++-- app/build.gradle | 4 ++-- fastlane/metadata/android/en-US/changelogs/32.txt | 3 +++ 4 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/32.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 11e08ad80..14f8092d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,12 @@ # Changelog -## dev +## 3.1.2 #### What's Improved - `artistssort`, `albumartistssort`, and `album_artists` tags are now recognized - Non-english digit strings are sorted more correctly - Reduced visual loading time +- Genre/artist/album information is now obtained by specific child items #### What's Fixed - Disc number is no longer mis-aligned when no subtitle is present diff --git a/README.md b/README.md index f5e5ebabe..ccb7bcb0d 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@

Auxio

A simple, rational music player for android.

- - Latest Version + + Latest Version Releases diff --git a/app/build.gradle b/app/build.gradle index bd85eca83..d0ddac776 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -20,8 +20,8 @@ android { defaultConfig { applicationId namespace - versionName "3.1.1" - versionCode 31 + versionName "3.1.2" + versionCode 32 minSdk 24 targetSdk 33 diff --git a/fastlane/metadata/android/en-US/changelogs/32.txt b/fastlane/metadata/android/en-US/changelogs/32.txt new file mode 100644 index 000000000..c43912c26 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/32.txt @@ -0,0 +1,3 @@ +Auxio 3.1.0 introduces playlisting functionality, with more features coming soon. +This release adds some minor UI fixes and quality of life improvements. +For more information, see https://github.com/OxygenCobalt/Auxio/releases/tag/v3.1.2. \ No newline at end of file