diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index f0aff366e..f106a3aac 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -23,8 +23,8 @@ jobs: cache: gradle - name: Grant execute permission for gradlew run: chmod +x gradlew - # - name: Test app with Gradle - # run: ./gradlew app:testDebug + - name: Test app with Gradle + run: ./gradlew app:testDebug - name: Build debug APK with Gradle run: ./gradlew app:packageDebug - name: Upload debug APK artifact diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fd76d375..1ecfb63b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## dev + +#### What's New +- Added ability to rewind/skip tracks by swiping back/forward + +#### What's Improved +- Added support for native M4A multi-value tags based on duplicate atoms + +#### What's Fixed +- Fixed app restart being required when changing intelligent sorting +or music separator settings +- Fixed widget/notification actions not working on Android 14 +- Fixed app crash when using hebrew language +- Fixed app crash when adding to a playlist while in the playlist detail view +- Fixed music loading failing in some cases on Android 14 + ## 3.2.0 #### What's New @@ -16,6 +32,10 @@ aspect ratio setting #### What's Fixed - Playlist detail view now respects playback settings + +#### Dev/Meta +- Revamped navigation backend + ## 3.1.4 #### What's Fixed diff --git a/README.md b/README.md index 8b16f36ed..4c867895b 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,6 @@ Auxio is a local music player with a fast, reliable UI/UX without the many useless features present in other music players. Built off of modern media playback libraries, Auxio has superior library support and listening quality compared to other apps that use outdated android functionality. In short, **It plays music.** -I primarily built Auxio for myself, but you can use it too, I guess. - **The default branch is the development version of the repository. For a stable version, see the master branch.** ## Screenshots @@ -60,12 +58,12 @@ precise/original dates, sort tags, and more - Headset autoplay - Stylish widgets that automatically adapt to their size - Completely private and offline -- No rounded album covers (Unless you want them. Then you can.) +- No rounded album covers (by default) ## Permissions -- Storage (`READ_MEDIA_AUDIO`, `READ_EXTERNAL_STORAGE`) to read and play your media files -- Services (`FOREGROUND_SERVICE`, `WAKE_LOCK`) to keep the music playing even if the app itself is in background +- Storage (`READ_MEDIA_AUDIO`, `READ_EXTERNAL_STORAGE`) to read and play your music files +- Services (`FOREGROUND_SERVICE`, `WAKE_LOCK`) to keep the music playing in the background ## Building diff --git a/app/build.gradle b/app/build.gradle index 6ab20ef92..f6a0d3abc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -21,8 +21,8 @@ android { defaultConfig { applicationId namespace - versionName "3.2.0" - versionCode 35 + versionName "3.2.1" + versionCode 36 minSdk 24 targetSdk 34 @@ -85,9 +85,9 @@ dependencies { // --- SUPPORT --- // General - implementation "androidx.core:core-ktx:1.10.1" + implementation "androidx.core:core-ktx:1.12.0" implementation "androidx.appcompat:appcompat:1.6.1" - implementation "androidx.activity:activity-ktx:1.7.2" + implementation "androidx.activity:activity-ktx:1.8.0" implementation "androidx.fragment:fragment-ktx:1.6.1" // Components @@ -100,7 +100,7 @@ dependencies { implementation "androidx.viewpager2:viewpager2:1.0.0" // Lifecycle - def lifecycle_version = "2.6.1" + def lifecycle_version = "2.6.2" implementation "androidx.lifecycle:lifecycle-common:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" @@ -117,7 +117,7 @@ dependencies { implementation "androidx.preference:preference-ktx:1.2.1" // Database - def room_version = '2.6.0-alpha03' + def room_version = '2.6.0-rc01' implementation "androidx.room:room-runtime:$room_version" ksp "androidx.room:room-compiler:$room_version" implementation "androidx.room:room-ktx:$room_version" @@ -134,7 +134,7 @@ dependencies { // Material // TODO: Exactly figure out the conditions that the 1.7.0 ripple bug occurred so you can just // PR a fix. - implementation "com.google.android.material:material:1.10.0-alpha06" + implementation "com.google.android.material:material:1.10.0" // Dependency Injection implementation "com.google.dagger:dagger:$hilt_version" @@ -142,9 +142,15 @@ dependencies { implementation "com.google.dagger:hilt-android:$hilt_version" kapt "com.google.dagger:hilt-android-compiler:$hilt_version" + // Logging + implementation 'com.jakewharton.timber:timber:5.0.1' + // Testing debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12' testImplementation "junit:junit:4.13.2" + testImplementation "io.mockk:mockk:1.13.7" + testImplementation "org.robolectric:robolectric:4.9" + testImplementation 'androidx.test:core-ktx:1.5.0' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' } diff --git a/app/src/androidTest/java/org/oxycblt/auxio/StubTest.kt b/app/src/androidTest/java/org/oxycblt/auxio/StubTest.kt deleted file mode 100644 index a0ba54a3d..000000000 --- a/app/src/androidTest/java/org/oxycblt/auxio/StubTest.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2023 Auxio Project - * StubTest.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 - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import org.junit.Assert.* -import org.junit.Test -import org.junit.runner.RunWith - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class StubTest { - // TODO: Make tests - @Test - fun useAppContext() { - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("org.oxycblt.auxio.debug", appContext.packageName) - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/Auxio.kt b/app/src/main/java/org/oxycblt/auxio/Auxio.kt index df737e4c2..ebcffb5e2 100644 --- a/app/src/main/java/org/oxycblt/auxio/Auxio.kt +++ b/app/src/main/java/org/oxycblt/auxio/Auxio.kt @@ -29,6 +29,7 @@ import org.oxycblt.auxio.home.HomeSettings import org.oxycblt.auxio.image.ImageSettings import org.oxycblt.auxio.playback.PlaybackSettings import org.oxycblt.auxio.ui.UISettings +import timber.log.Timber /** * A simple, rational music player for android. @@ -44,6 +45,10 @@ class Auxio : Application() { override fun onCreate() { super.onCreate() + if (BuildConfig.DEBUG) { + Timber.plant(Timber.DebugTree()) + } + // Migrate any settings that may have changed in an app update. imageSettings.migrate() playbackSettings.migrate() diff --git a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt index 725f60444..c98d89cdd 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt @@ -68,8 +68,8 @@ class MainActivity : AppCompatActivity() { logD("Activity created") } - override fun onStart() { - super.onStart() + override fun onResume() { + super.onResume() startService(Intent(this, IndexerService::class.java)) startService(Intent(this, PlaybackService::class.java)) diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index 297bad95c..ed1b47c7a 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -204,6 +204,10 @@ class MainFragment : } override fun onPreDraw(): Boolean { + // TODO: Due to draw caching even *this* isn't effective enough to avoid the bottom + // sheets continually getting stuck. I need something with even more frequent updates, + // or otherwise bottom sheets get stuck. + // We overload CoordinatorLayout far too much to rely on any of it's typical // listener functionality. Just update all transitions before every draw. Should // probably be cheap enough. diff --git a/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt index ed460bc33..540017724 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt @@ -328,7 +328,11 @@ class PlaylistDetailFragment : logD("Deleting ${decision.playlist}") PlaylistDetailFragmentDirections.deletePlaylist(decision.playlist.uid) } - is PlaylistDecision.Add, + is PlaylistDecision.Add -> { + logD("Adding ${decision.songs.size} songs to a playlist") + PlaylistDetailFragmentDirections.addToPlaylist( + decision.songs.map { it.uid }.toTypedArray()) + } is PlaylistDecision.New -> error("Unexpected playlist decision $decision") } findNavController().navigateSafe(directions) diff --git a/app/src/main/java/org/oxycblt/auxio/home/FlipFloatingActionButton.kt b/app/src/main/java/org/oxycblt/auxio/home/FlipFloatingActionButton.kt index a03adccfd..c3cd4a82f 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/FlipFloatingActionButton.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/FlipFloatingActionButton.kt @@ -51,7 +51,7 @@ constructor( // Apply the new configuration possibly set in flipTo. This should occur even if // a flip was canceled by a hide. pendingConfig?.run { - this@FlipFloatingActionButton.logD("Applying pending configuration") + logD("Applying pending configuration") setImageResource(iconRes) contentDescription = context.getString(contentDescriptionRes) setOnClickListener(clickListener) diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt index f2930d3ec..488c98126 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt @@ -43,7 +43,7 @@ interface MusicSettings : Settings { /** Whether to be actively watching for changes in the music library. */ val shouldBeObserving: Boolean /** A [String] of characters representing the desired characters to denote multi-value tags. */ - var multiValueSeparators: String + var separators: String /** Whether to enable more advanced sorting by articles and numbers. */ val intelligentSorting: Boolean @@ -85,7 +85,7 @@ class MusicSettingsImpl @Inject constructor(@ApplicationContext context: Context override val shouldBeObserving: Boolean get() = sharedPreferences.getBoolean(getString(R.string.set_key_observing), false) - override var multiValueSeparators: String + override var separators: String // Differ from convention and store a string of separator characters instead of an int // code. This makes it easier to use and more extendable. get() = sharedPreferences.getString(getString(R.string.set_key_separators), "") ?: "" 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 d28547239..9eb52bbc6 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,7 +32,7 @@ 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 = 34, exportSchema = false) +@Database(entities = [CachedSong::class], version = 36, exportSchema = false) abstract class CacheDatabase : RoomDatabase() { abstract fun cachedSongsDao(): CachedSongsDao } @@ -63,9 +63,9 @@ data class CachedSong( /** @see RawSong */ var durationMs: Long, /** @see RawSong.replayGainTrackAdjustment */ - val replayGainTrackAdjustment: Float?, + val replayGainTrackAdjustment: Float? = null, /** @see RawSong.replayGainAlbumAdjustment */ - val replayGainAlbumAdjustment: Float?, + val replayGainAlbumAdjustment: Float? = null, /** @see RawSong.musicBrainzId */ var musicBrainzId: String? = null, /** @see RawSong.name */ 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 cd75ba578..527dcd198 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 @@ -32,6 +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.music.info.Name +import org.oxycblt.auxio.music.metadata.Separators import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.unlikelyToBeNull @@ -107,7 +109,7 @@ interface DeviceLibrary { */ suspend fun create( rawSongs: Channel, - processedSongs: Channel + processedSongs: Channel, ): DeviceLibraryImpl } } @@ -118,6 +120,9 @@ class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: Mu rawSongs: Channel, processedSongs: Channel ): DeviceLibraryImpl { + val nameFactory = Name.Known.Factory.from(musicSettings) + val separators = Separators.from(musicSettings) + val songGrouping = mutableMapOf() val albumGrouping = mutableMapOf>() val artistGrouping = mutableMapOf>() @@ -127,7 +132,7 @@ class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: Mu // All music information is grouped as it is indexed by other components. for (rawSong in rawSongs) { - val song = SongImpl(rawSong, musicSettings) + val song = SongImpl(rawSong, nameFactory, separators) // 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. @@ -207,7 +212,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.mapTo(mutableSetOf()) { AlbumImpl(it, musicSettings) } + val albums = albumGrouping.values.mapTo(mutableSetOf()) { AlbumImpl(it, nameFactory) } for (album in albums) { for (rawArtist in album.rawArtists) { val key = RawArtist.Key(rawArtist) @@ -243,8 +248,8 @@ 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.mapTo(mutableSetOf()) { ArtistImpl(it, musicSettings) } - val genres = genreGrouping.values.mapTo(mutableSetOf()) { GenreImpl(it, musicSettings) } + val artists = artistGrouping.values.mapTo(mutableSetOf()) { ArtistImpl(it, nameFactory) } + val genres = genreGrouping.values.mapTo(mutableSetOf()) { GenreImpl(it, nameFactory) } return DeviceLibraryImpl(songGrouping.values.toSet(), albums, artists, genres) } @@ -253,10 +258,10 @@ class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: Mu // TODO: Avoid redundant data creation class DeviceLibraryImpl( - override val songs: Set, - override val albums: Set, - override val artists: Set, - override val genres: Set + 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()) } } 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 1d2ce2a26..91a4e1702 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 @@ -25,7 +25,6 @@ import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Music -import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.music.MusicType import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.fs.MimeType @@ -36,8 +35,8 @@ 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.music.metadata.Separators 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.positiveOrNull import org.oxycblt.auxio.util.toUuidOrNull @@ -48,10 +47,15 @@ import org.oxycblt.auxio.util.update * Library-backed implementation of [Song]. * * @param rawSong The [RawSong] to derive the member data from. - * @param musicSettings [MusicSettings] to for user parsing configuration. + * @param nameFactory The [Name.Known.Factory] to interpret name information with. + * @param separators The [Separators] to parse multi-value tags with. * @author Alexander Capehart (OxygenCobalt) */ -class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Song { +class SongImpl( + private val rawSong: RawSong, + private val nameFactory: Name.Known.Factory, + private val separators: Separators +) : Song { override val uid = // Attempt to use a MusicBrainz ID first before falling back to a hashed UID. rawSong.musicBrainzId?.toUuidOrNull()?.let { Music.UID.musicBrainz(MusicType.SONGS, it) } @@ -70,67 +74,47 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son update(rawSong.albumArtistNames) } override val name = - Name.Known.from( - requireNotNull(rawSong.name) { "Invalid raw: No title" }, - rawSong.sortName, - musicSettings) + nameFactory.parse( + requireNotNull(rawSong.name) { "Invalid raw ${rawSong.fileName}: No title" }, + rawSong.sortName) override val track = rawSong.track override val disc = rawSong.disc?.let { Disc(it, rawSong.subtitle) } override val date = rawSong.date - override val uri = requireNotNull(rawSong.mediaStoreId) { "Invalid raw: No id" }.toAudioUri() + override val uri = + requireNotNull(rawSong.mediaStoreId) { "Invalid raw ${rawSong.fileName}: No id" } + .toAudioUri() override val path = Path( - name = requireNotNull(rawSong.fileName) { "Invalid raw: No display name" }, - parent = requireNotNull(rawSong.directory) { "Invalid raw: No parent directory" }) + name = + requireNotNull(rawSong.fileName) { + "Invalid raw ${rawSong.fileName}: No display name" + }, + parent = + requireNotNull(rawSong.directory) { + "Invalid raw ${rawSong.fileName}: No parent directory" + }) override val mimeType = MimeType( fromExtension = - requireNotNull(rawSong.extensionMimeType) { "Invalid raw: No mime type" }, + requireNotNull(rawSong.extensionMimeType) { + "Invalid raw ${rawSong.fileName}: No mime type" + }, fromFormat = null) - override val size = requireNotNull(rawSong.size) { "Invalid raw: No size" } - override val durationMs = requireNotNull(rawSong.durationMs) { "Invalid raw: No duration" } + override val size = requireNotNull(rawSong.size) { "Invalid raw ${rawSong.fileName}: No size" } + override val durationMs = + requireNotNull(rawSong.durationMs) { "Invalid raw ${rawSong.fileName}: No duration" } override val replayGainAdjustment = ReplayGainAdjustment( track = rawSong.replayGainTrackAdjustment, album = rawSong.replayGainAlbumAdjustment) - override val dateAdded = requireNotNull(rawSong.dateAdded) { "Invalid raw: No date added" } + override val dateAdded = + requireNotNull(rawSong.dateAdded) { "Invalid raw ${rawSong.fileName}: No date added" } + private var _album: AlbumImpl? = null override val album: Album get() = unlikelyToBeNull(_album) - private val hashCode = 31 * uid.hashCode() + rawSong.hashCode() - - override fun hashCode() = hashCode - - override fun equals(other: Any?) = - other is SongImpl && uid == other.uid && rawSong == other.rawSong - - override fun toString() = "Song(uid=$uid, name=$name)" - - private val artistMusicBrainzIds = rawSong.artistMusicBrainzIds.parseMultiValue(musicSettings) - private val artistNames = rawSong.artistNames.parseMultiValue(musicSettings) - private val artistSortNames = rawSong.artistSortNames.parseMultiValue(musicSettings) - private val rawIndividualArtists = - artistNames.mapIndexed { i, name -> - RawArtist( - artistMusicBrainzIds.getOrNull(i)?.toUuidOrNull(), - name, - artistSortNames.getOrNull(i)) - } - - private val albumArtistMusicBrainzIds = - rawSong.albumArtistMusicBrainzIds.parseMultiValue(musicSettings) - private val albumArtistNames = rawSong.albumArtistNames.parseMultiValue(musicSettings) - private val albumArtistSortNames = rawSong.albumArtistSortNames.parseMultiValue(musicSettings) - private val rawAlbumArtists = - albumArtistNames.mapIndexed { i, name -> - RawArtist( - albumArtistMusicBrainzIds.getOrNull(i)?.toUuidOrNull(), - name, - albumArtistSortNames.getOrNull(i)) - } - private val _artists = mutableListOf() override val artists: List get() = _artists @@ -143,40 +127,95 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son * The [RawAlbum] instances collated by the [Song]. This can be used to group [Song]s into an * [Album]. */ - val rawAlbum = - RawAlbum( - mediaStoreId = requireNotNull(rawSong.albumMediaStoreId) { "Invalid raw: No album id" }, - musicBrainzId = rawSong.albumMusicBrainzId?.toUuidOrNull(), - name = requireNotNull(rawSong.albumName) { "Invalid raw: No album name" }, - sortName = rawSong.albumSortName, - releaseType = ReleaseType.parse(rawSong.releaseTypes.parseMultiValue(musicSettings)), - rawArtists = - rawAlbumArtists - .ifEmpty { rawIndividualArtists } - .distinctBy { it.key } - .ifEmpty { listOf(RawArtist(null, null)) }) + val rawAlbum: RawAlbum /** * The [RawArtist] instances collated by the [Song]. The artists of the song take priority, * followed by the album artists. If there are no artists, this field will be a single "unknown" * [RawArtist]. This can be used to group up [Song]s into an [Artist]. */ - val rawArtists = - rawIndividualArtists - .ifEmpty { rawAlbumArtists } - .distinctBy { it.key } - .ifEmpty { listOf(RawArtist()) } + val rawArtists: List /** * The [RawGenre] instances collated by the [Song]. This can be used to group up [Song]s into a * [Genre]. ID3v2 Genre names are automatically converted to their resolved names. */ - val rawGenres = - rawSong.genreNames - .parseId3GenreNames(musicSettings) - .map { RawGenre(it) } - .distinctBy { it.key } - .ifEmpty { listOf(RawGenre()) } + val rawGenres: List + + private var hashCode: Int = uid.hashCode() + + init { + val artistMusicBrainzIds = separators.split(rawSong.artistMusicBrainzIds) + val artistNames = separators.split(rawSong.artistNames) + val artistSortNames = separators.split(rawSong.artistSortNames) + val rawIndividualArtists = + artistNames + .mapIndexedTo(mutableSetOf()) { i, name -> + RawArtist( + artistMusicBrainzIds.getOrNull(i)?.toUuidOrNull(), + name, + artistSortNames.getOrNull(i)) + } + .toList() + + val albumArtistMusicBrainzIds = separators.split(rawSong.albumArtistMusicBrainzIds) + val albumArtistNames = separators.split(rawSong.albumArtistNames) + val albumArtistSortNames = separators.split(rawSong.albumArtistSortNames) + val rawAlbumArtists = + albumArtistNames + .mapIndexedTo(mutableSetOf()) { i, name -> + RawArtist( + albumArtistMusicBrainzIds.getOrNull(i)?.toUuidOrNull(), + name, + albumArtistSortNames.getOrNull(i)) + } + .toList() + + rawAlbum = + RawAlbum( + mediaStoreId = + requireNotNull(rawSong.albumMediaStoreId) { + "Invalid raw ${rawSong.fileName}: No album id" + }, + musicBrainzId = rawSong.albumMusicBrainzId?.toUuidOrNull(), + name = + requireNotNull(rawSong.albumName) { + "Invalid raw ${rawSong.fileName}: No album name" + }, + sortName = rawSong.albumSortName, + releaseType = ReleaseType.parse(separators.split(rawSong.releaseTypes)), + rawArtists = + rawAlbumArtists + .ifEmpty { rawIndividualArtists } + .ifEmpty { listOf(RawArtist()) }) + + rawArtists = + rawIndividualArtists.ifEmpty { rawAlbumArtists }.ifEmpty { listOf(RawArtist()) } + + val genreNames = + (rawSong.genreNames.parseId3GenreNames() ?: separators.split(rawSong.genreNames)) + rawGenres = + genreNames + .mapTo(mutableSetOf()) { RawGenre(it) } + .toList() + .ifEmpty { listOf(RawGenre()) } + + hashCode = 31 * hashCode + rawSong.hashCode() + hashCode = 31 * hashCode + nameFactory.hashCode() + } + + override fun hashCode() = hashCode + + // Since equality on public-facing music models is not identical to the tag equality, + // we just compare raw instances and how they are interpreted. + override fun equals(other: Any?) = + other is SongImpl && + uid == other.uid && + nameFactory == other.nameFactory && + separators == other.separators && + rawSong == other.rawSong + + override fun toString() = "Song(uid=$uid, name=$name)" /** * Links this [Song] with a parent [Album]. @@ -211,10 +250,12 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son * @return This instance upcasted to [Song]. */ fun finalize(): Song { - checkNotNull(_album) { "Malformed song: No album" } + checkNotNull(_album) { "Malformed song ${path.name}: No album" } - check(_artists.isNotEmpty()) { "Malformed song: No artists" } - check(_artists.size == rawArtists.size) { "Malformed song: Artist grouping mismatch" } + check(_artists.isNotEmpty()) { "Malformed song ${path.name}: No artists" } + check(_artists.size == rawArtists.size) { + "Malformed song ${path.name}: Artist grouping mismatch" + } for (i in _artists.indices) { // Non-destructively reorder the linked artists so that they align with // the artist ordering within the song metadata. @@ -224,8 +265,10 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son _artists[i] = other } - check(_genres.isNotEmpty()) { "Malformed song: No genres" } - check(_genres.size == rawGenres.size) { "Malformed song: Genre grouping mismatch" } + check(_genres.isNotEmpty()) { "Malformed song ${path.name}: No genres" } + check(_genres.size == rawGenres.size) { + "Malformed song ${path.name}: Genre grouping mismatch" + } for (i in _genres.indices) { // Non-destructively reorder the linked genres so that they align with // the genre ordering within the song metadata. @@ -242,12 +285,12 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son * Library-backed implementation of [Album]. * * @param grouping [Grouping] to derive the member data from. - * @param musicSettings [MusicSettings] to for user parsing configuration. + * @param nameFactory The [Name.Known.Factory] to interpret name information with. * @author Alexander Capehart (OxygenCobalt) */ class AlbumImpl( grouping: Grouping, - musicSettings: MusicSettings, + private val nameFactory: Name.Known.Factory ) : Album { private val rawAlbum = grouping.raw.inner @@ -261,7 +304,7 @@ class AlbumImpl( update(rawAlbum.name) update(rawAlbum.rawArtists.map { it.name }) } - override val name = Name.Known.from(rawAlbum.name, rawAlbum.sortName, musicSettings) + override val name = nameFactory.parse(rawAlbum.name, rawAlbum.sortName) override val dates: Date.Range? override val releaseType = rawAlbum.releaseType ?: ReleaseType.Album(null) override val coverUri = CoverUri(rawAlbum.mediaStoreId.toCoverUri(), grouping.raw.src.uri) @@ -311,13 +354,20 @@ class AlbumImpl( dateAdded = earliestDateAdded hashCode = 31 * hashCode + rawAlbum.hashCode() + hashCode = 31 * hashCode + nameFactory.hashCode() hashCode = 31 * hashCode + songs.hashCode() } override fun hashCode() = hashCode + // Since equality on public-facing music models is not identical to the tag equality, + // we just compare raw instances and how they are interpreted. override fun equals(other: Any?) = - other is AlbumImpl && uid == other.uid && rawAlbum == other.rawAlbum && songs == other.songs + other is AlbumImpl && + uid == other.uid && + rawAlbum == other.rawAlbum && + nameFactory == other.nameFactory && + songs == other.songs override fun toString() = "Album(uid=$uid, name=$name)" @@ -343,9 +393,11 @@ class AlbumImpl( * @return This instance upcasted to [Album]. */ fun finalize(): Album { - check(songs.isNotEmpty()) { "Malformed album: Empty" } - check(_artists.isNotEmpty()) { "Malformed album: No artists" } - check(_artists.size == rawArtists.size) { "Malformed album: Artist grouping mismatch" } + check(songs.isNotEmpty()) { "Malformed album $name: Empty" } + check(_artists.isNotEmpty()) { "Malformed album $name: No artists" } + check(_artists.size == rawArtists.size) { + "Malformed album $name: Artist grouping mismatch" + } for (i in _artists.indices) { // Non-destructively reorder the linked artists so that they align with // the artist ordering within the song metadata. @@ -362,10 +414,13 @@ class AlbumImpl( * Library-backed implementation of [Artist]. * * @param grouping [Grouping] to derive the member data from. - * @param musicSettings [MusicSettings] to for user parsing configuration. + * @param nameFactory The [Name.Known.Factory] to interpret name information with. * @author Alexander Capehart (OxygenCobalt) */ -class ArtistImpl(grouping: Grouping, musicSettings: MusicSettings) : Artist { +class ArtistImpl( + grouping: Grouping, + private val nameFactory: Name.Known.Factory +) : Artist { private val rawArtist = grouping.raw.inner override val uid = @@ -373,7 +428,7 @@ class ArtistImpl(grouping: Grouping, musicSettings: MusicSetti rawArtist.musicBrainzId?.let { Music.UID.musicBrainz(MusicType.ARTISTS, it) } ?: Music.UID.auxio(MusicType.ARTISTS) { update(rawArtist.name) } override val name = - rawArtist.name?.let { Name.Known.from(it, rawArtist.sortName, musicSettings) } + rawArtist.name?.let { nameFactory.parse(it, rawArtist.sortName) } ?: Name.Unknown(R.string.def_artist) override val songs: Set @@ -403,7 +458,7 @@ class ArtistImpl(grouping: Grouping, musicSettings: MusicSetti music.link(this) albumMap[music] = true } - else -> error("Unexpected input music ${music::class.simpleName}") + else -> error("Unexpected input music $music in $name ${music::class.simpleName}") } } @@ -414,6 +469,7 @@ class ArtistImpl(grouping: Grouping, musicSettings: MusicSetti durationMs = songs.sumOf { it.durationMs }.positiveOrNull() hashCode = 31 * hashCode + rawArtist.hashCode() + hashCode = 31 * hashCode + nameFactory.hashCode() hashCode = 31 * hashCode + songs.hashCode() } @@ -421,10 +477,13 @@ class ArtistImpl(grouping: Grouping, musicSettings: MusicSetti // the same UID but different songs are not equal. override fun hashCode() = hashCode + // Since equality on public-facing music models is not identical to the tag equality, + // we just compare raw instances and how they are interpreted. override fun equals(other: Any?) = other is ArtistImpl && uid == other.uid && rawArtist == other.rawArtist && + nameFactory == other.nameFactory && songs == other.songs override fun toString() = "Artist(uid=$uid, name=$name)" @@ -447,7 +506,7 @@ class ArtistImpl(grouping: Grouping, musicSettings: MusicSetti * @return This instance upcasted to [Artist]. */ fun finalize(): Artist { - check(songs.isNotEmpty() || albums.isNotEmpty()) { "Malformed artist: Empty" } + check(songs.isNotEmpty() || albums.isNotEmpty()) { "Malformed artist $name: Empty" } genres = Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING) .genres(songs.flatMapTo(mutableSetOf()) { it.genres }) @@ -459,15 +518,18 @@ class ArtistImpl(grouping: Grouping, musicSettings: MusicSetti * Library-backed implementation of [Genre]. * * @param grouping [Grouping] to derive the member data from. - * @param musicSettings [MusicSettings] to for user parsing configuration. + * @param nameFactory The [Name.Known.Factory] to interpret name information with. * @author Alexander Capehart (OxygenCobalt) */ -class GenreImpl(grouping: Grouping, musicSettings: MusicSettings) : Genre { +class GenreImpl( + grouping: Grouping, + private val nameFactory: Name.Known.Factory +) : Genre { private val rawGenre = grouping.raw.inner override val uid = Music.UID.auxio(MusicType.GENRES) { update(rawGenre.name) } override val name = - rawGenre.name?.let { Name.Known.from(it, rawGenre.name, musicSettings) } + rawGenre.name?.let { nameFactory.parse(it, rawGenre.name) } ?: Name.Unknown(R.string.def_genre) override val songs: Set @@ -491,13 +553,18 @@ class GenreImpl(grouping: Grouping, musicSettings: MusicSett durationMs = totalDuration hashCode = 31 * hashCode + rawGenre.hashCode() + hashCode = 31 * hashCode + nameFactory.hashCode() hashCode = 31 * hashCode + songs.hashCode() } override fun hashCode() = hashCode override fun equals(other: Any?) = - other is GenreImpl && uid == other.uid && rawGenre == other.rawGenre && songs == other.songs + other is GenreImpl && + uid == other.uid && + rawGenre == other.rawGenre && + nameFactory == other.nameFactory && + songs == other.songs override fun toString() = "Genre(uid=$uid, name=$name)" @@ -519,7 +586,7 @@ class GenreImpl(grouping: Grouping, musicSettings: MusicSett * @return This instance upcasted to [Genre]. */ fun finalize(): Genre { - check(songs.isNotEmpty()) { "Malformed genre: Empty" } + check(songs.isNotEmpty()) { "Malformed genre $name: Empty" } return this } } 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 bbde5aca3..09f4d8035 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 @@ -20,6 +20,7 @@ package org.oxycblt.auxio.music.info import android.content.Context import androidx.annotation.StringRes +import androidx.annotation.VisibleForTesting import java.text.CollationKey import java.text.Collator import org.oxycblt.auxio.music.MusicSettings @@ -54,36 +55,7 @@ sealed interface Name : Comparable { abstract val sort: String? /** A tokenized version of the name that will be compared. */ - protected abstract val sortTokens: List - - /** An individual part of a name string that can be compared intelligently. */ - protected data class SortToken(val collationKey: CollationKey, val type: Type) : - Comparable { - override fun compareTo(other: SortToken): Int { - // Numeric tokens should always be lower than lexicographic tokens. - val modeComp = type.compareTo(other.type) - if (modeComp != 0) { - return modeComp - } - - // Numeric strings must be ordered by magnitude, thus immediately short-circuit - // the comparison if the lengths do not match. - if (type == Type.NUMERIC && - collationKey.sourceString.length != other.collationKey.sourceString.length) { - return collationKey.sourceString.length - other.collationKey.sourceString.length - } - - return collationKey.compareTo(other.collationKey) - } - - /** Denotes the type of comparison to be performed with this token. */ - enum class Type { - /** Compare as a digit string, like "65". */ - NUMERIC, - /** Compare as a standard alphanumeric string, like "65daysofstatic" */ - LEXICOGRAPHIC - } - } + @VisibleForTesting(VisibleForTesting.PROTECTED) abstract val sortTokens: List final override val thumb: String get() = @@ -108,20 +80,30 @@ sealed interface Name : Comparable { is Unknown -> 1 } - companion object { + interface Factory { /** * Create a new instance of [Name.Known] * * @param raw The raw name obtained from the music item * @param sort The raw sort name obtained from the music item - * @param musicSettings [MusicSettings] required for name configuration. */ - fun from(raw: String, sort: String?, musicSettings: MusicSettings): Known = - if (musicSettings.intelligentSorting) { - IntelligentKnownName(raw, sort) - } else { - SimpleKnownName(raw, sort) - } + fun parse(raw: String, sort: String?): Known + + companion object { + /** + * Creates a new instance from the **current state** of the given [MusicSettings]'s + * user-defined name configuration. + * + * @param settings The [MusicSettings] to use. + * @return A [Factory] instance reflecting the configuration state. + */ + fun from(settings: MusicSettings) = + if (settings.intelligentSorting) { + IntelligentKnownName.Factory + } else { + SimpleKnownName.Factory + } + } } } @@ -148,22 +130,28 @@ sealed interface Name : Comparable { private val collator: Collator = Collator.getInstance().apply { strength = Collator.PRIMARY } private val punctRegex by lazy { Regex("[\\p{Punct}+]") } +// TODO: Consider how you want to handle whitespace and "gaps" in names. + /** * Plain [Name.Known] implementation that is internationalization-safe. * * @author Alexander Capehart (OxygenCobalt) */ -private data class SimpleKnownName(override val raw: String, override val sort: String?) : - Name.Known() { +@VisibleForTesting +data class SimpleKnownName(override val raw: String, override val sort: String?) : Name.Known() { override val sortTokens = listOf(parseToken(sort ?: raw)) private fun parseToken(name: String): SortToken { // Remove excess punctuation from the string, as those usually aren't considered in sorting. - val stripped = name.replace(punctRegex, "").ifEmpty { name } + val stripped = name.replace(punctRegex, "").trim().ifEmpty { name } val collationKey = collator.getCollationKey(stripped) // Always use lexicographic mode since we aren't parsing any numeric components return SortToken(collationKey, SortToken.Type.LEXICOGRAPHIC) } + + data object Factory : Name.Known.Factory { + override fun parse(raw: String, sort: String?) = SimpleKnownName(raw, sort) + } } /** @@ -171,7 +159,8 @@ private data class SimpleKnownName(override val raw: String, override val sort: * * @author Alexander Capehart (OxygenCobalt) */ -private data class IntelligentKnownName(override val raw: String, override val sort: String?) : +@VisibleForTesting +data class IntelligentKnownName(override val raw: String, override val sort: String?) : Name.Known() { override val sortTokens = parseTokens(sort ?: raw) @@ -180,7 +169,8 @@ private data class IntelligentKnownName(override val raw: String, override val s // optimize it val stripped = name - // Remove excess punctuation from the string, as those u + // Remove excess punctuation from the string, as those usually aren't + // considered in sorting. .replace(punctRegex, "") .ifEmpty { name } .run { @@ -218,7 +208,40 @@ private data class IntelligentKnownName(override val raw: String, override val s } } + data object Factory : Name.Known.Factory { + override fun parse(raw: String, sort: String?) = IntelligentKnownName(raw, sort) + } + companion object { private val TOKEN_REGEX by lazy { Regex("(\\d+)|(\\D+)") } } } + +/** An individual part of a name string that can be compared intelligently. */ +@VisibleForTesting(VisibleForTesting.PROTECTED) +data class SortToken(val collationKey: CollationKey, val type: Type) : Comparable { + override fun compareTo(other: SortToken): Int { + // Numeric tokens should always be lower than lexicographic tokens. + val modeComp = type.compareTo(other.type) + if (modeComp != 0) { + return modeComp + } + + // Numeric strings must be ordered by magnitude, thus immediately short-circuit + // the comparison if the lengths do not match. + if (type == Type.NUMERIC && + collationKey.sourceString.length != other.collationKey.sourceString.length) { + return collationKey.sourceString.length - other.collationKey.sourceString.length + } + + return collationKey.compareTo(other.collationKey) + } + + /** Denotes the type of comparison to be performed with this token. */ + enum class Type { + /** Compare as a digit string, like "65". */ + NUMERIC, + /** Compare as a standard alphanumeric string, like "65daysofstatic" */ + LEXICOGRAPHIC + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/Separators.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/Separators.kt new file mode 100644 index 000000000..8d2740e74 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/Separators.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2023 Auxio Project + * Separators.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.music.metadata + +import androidx.annotation.VisibleForTesting +import org.oxycblt.auxio.music.MusicSettings + +/** + * Defines the user-specified parsing of multi-value tags. This should be used to parse any tags + * that may be delimited with a separator character. + * + * @author Alexander Capehart (OxygenCobalt) + */ +interface Separators { + /** + * Parse a separated value from one or more strings. If the value is already composed of more + * than one value, nothing is done. Otherwise, it will attempt to split it based on the user's + * separator preferences. + * + * @return A new list of one or more [String]s parsed by the separator configuration + */ + fun split(strings: List): List + + companion object { + const val COMMA = ',' + const val SEMICOLON = ';' + const val SLASH = '/' + const val PLUS = '+' + const val AND = '&' + + /** + * Creates a new instance from the **current state** of the given [MusicSettings]'s + * user-defined separator configuration. + * + * @param settings The [MusicSettings] to use. + * @return A new [Separators] instance reflecting the configuration state. + */ + fun from(settings: MusicSettings) = from(settings.separators) + + @VisibleForTesting + fun from(chars: String) = + if (chars.isNotEmpty()) { + CharSeparators(chars.toSet()) + } else { + NoSeparators + } + } +} + +private data class CharSeparators(private val chars: Set) : Separators { + override fun split(strings: List) = + if (strings.size == 1) splitImpl(strings.first()) else strings + + private fun splitImpl(string: String) = + string.splitEscaped { chars.contains(it) }.correctWhitespace() +} + +private object NoSeparators : Separators { + override fun split(strings: List) = strings +} diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/SeparatorsDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/SeparatorsDialog.kt index d74b9ba53..31195c408 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/SeparatorsDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/SeparatorsDialog.kt @@ -52,7 +52,7 @@ class SeparatorsDialog : ViewBindingMaterialDialogFragment - musicSettings.multiValueSeparators = getCurrentSeparators() + musicSettings.separators = getCurrentSeparators() } } @@ -68,8 +68,7 @@ class SeparatorsDialog : ViewBindingMaterialDialogFragment binding.separatorComma.isChecked = true @@ -102,14 +101,6 @@ class SeparatorsDialog : ViewBindingMaterialDialogFragment.parseMultiValue(settings: MusicSettings) = - if (size == 1) { - first().maybeParseBySeparators(settings) - } else { - // Nothing to do. - this - } - // TODO: Remove the escaping checks, it's too expensive to do this for every single tag. +// TODO: I want to eventually be able to move a lot of this into TagWorker once I no longer have +// to deal with the cross-module dependencies of MediaStoreExtractor. + /** * Split a [String] by the given selector, automatically handling escaped characters that satisfy * the selector. @@ -101,17 +87,6 @@ fun String.correctWhitespace() = trim().ifBlank { null } */ fun List.correctWhitespace() = mapNotNull { it.correctWhitespace() } -/** - * Attempt to parse a string by the user's separator preferences. - * - * @param settings [MusicSettings] required to obtain user separator configuration. - * @return A list of one or more [String]s that were split up by the user-defined separators. - */ -private fun String.maybeParseBySeparators(settings: MusicSettings): List { - if (settings.multiValueSeparators.isEmpty()) return listOf(this) - return splitEscaped { settings.multiValueSeparators.contains(it) }.correctWhitespace() -} - /// --- ID3v2 PARSING --- /** @@ -165,12 +140,12 @@ fun transformPositionField(pos: Int?, total: Int?) = * representations of genre fields into their named counterparts, and split up singular ID3v2-style * integer genre fields into one or more genres. * - * @param settings [MusicSettings] required to obtain user separator configuration. - * @return A list of one or more genre names.. + * @return A list of one or more genre names, or null if this multi-value list has no valid + * formatting. */ -fun List.parseId3GenreNames(settings: MusicSettings) = +fun List.parseId3GenreNames() = if (size == 1) { - first().parseId3MultiValueGenre(settings) + first().parseId3MultiValueGenre() } else { // Nothing to split, just map any ID3v1 genres to their name counterparts. map { it.parseId3v1Genre() ?: it } @@ -179,11 +154,10 @@ fun List.parseId3GenreNames(settings: MusicSettings) = /** * Parse a single ID3v1/ID3v2 integer genre field into their named representations. * - * @param settings [MusicSettings] required to obtain user separator configuration. - * @return A list of one or more genre names. + * @return list of one or more genre names, or null if this is not in ID3v2 format. */ -private fun String.parseId3MultiValueGenre(settings: MusicSettings) = - parseId3v1Genre()?.let { listOf(it) } ?: parseId3v2Genre() ?: maybeParseBySeparators(settings) +private fun String.parseId3MultiValueGenre() = + parseId3v1Genre()?.let { listOf(it) } ?: parseId3v2Genre() /** * Parse an ID3v1 integer genre field. 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 fae02585e..196c7c0dc 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 @@ -77,7 +77,6 @@ private class TagWorkerImpl( private val rawSong: RawSong, private val future: Future ) : TagWorker { - override fun poll(): RawSong? { if (!future.isDone) { // Not done yet, nothing to do. 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 a3d916b69..7e8c87391 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,11 +28,9 @@ 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>() + private val _id3v2 = mutableMapOf>() /** The ID3v2 text identification frames found in the file. Can have more than one value. */ val id3v2: Map> get() = _id3v2 @@ -53,7 +51,11 @@ class TextTags(metadata: Metadata) { ?: tag.id.sanitize() val values = tag.values.map { it.sanitize() }.correctWhitespace() if (values.isNotEmpty()) { - _id3v2[id] = values + // Normally, duplicate ID3v2 frames are forbidden. But since MP4 atoms, + // which can also have duplicates, are mapped to ID3v2 frames by ExoPlayer, + // we must drop this invariant and gracefully treat duplicates as if they + // are another way of specfiying multi-value tags. + _id3v2.getOrPut(id) { mutableListOf() }.addAll(values) } } is InternalFrame -> { @@ -62,7 +64,7 @@ class TextTags(metadata: Metadata) { val id = "TXXX:${tag.description.sanitize().lowercase()}" val value = tag.text if (value.isNotEmpty()) { - _id3v2[id] = listOf(value) + _id3v2.getOrPut(id) { mutableListOf() }.add(value) } } is VorbisComment -> { diff --git a/app/src/main/java/org/oxycblt/auxio/music/user/PlaylistImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/user/PlaylistImpl.kt index ffe7a5174..fe4418894 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/user/PlaylistImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/user/PlaylistImpl.kt @@ -19,7 +19,6 @@ package org.oxycblt.auxio.music.user import org.oxycblt.auxio.music.Music -import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.music.MusicType import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Song @@ -51,10 +50,10 @@ private constructor( * Clone the data in this instance to a new [PlaylistImpl] with the given [name]. * * @param name The new name to use. - * @param musicSettings [MusicSettings] required for name configuration. + * @param nameFactory The [Name.Known.Factory] to interpret name information with. */ - fun edit(name: String, musicSettings: MusicSettings) = - PlaylistImpl(uid, Name.Known.from(name, null, musicSettings), songs) + fun edit(name: String, nameFactory: Name.Known.Factory) = + PlaylistImpl(uid, nameFactory.parse(name, null), songs) /** * Clone the data in this instance to a new [PlaylistImpl] with the given [Song]s. @@ -76,29 +75,26 @@ private constructor( * * @param name The name of the playlist. * @param songs The songs to initially populate the playlist with. - * @param musicSettings [MusicSettings] required for name configuration. + * @param nameFactory The [Name.Known.Factory] to interpret name information with. */ - fun from(name: String, songs: List, musicSettings: MusicSettings) = - PlaylistImpl( - Music.UID.auxio(MusicType.PLAYLISTS), - Name.Known.from(name, null, musicSettings), - songs) + fun from(name: String, songs: List, nameFactory: Name.Known.Factory) = + PlaylistImpl(Music.UID.auxio(MusicType.PLAYLISTS), nameFactory.parse(name, null), songs) /** * Populate a new instance from a read [RawPlaylist]. * * @param rawPlaylist The [RawPlaylist] to read from. * @param deviceLibrary The [DeviceLibrary] to initialize from. - * @param musicSettings [MusicSettings] required for name configuration. + * @param nameFactory The [Name.Known.Factory] to interpret name information with. */ fun fromRaw( rawPlaylist: RawPlaylist, deviceLibrary: DeviceLibrary, - musicSettings: MusicSettings + nameFactory: Name.Known.Factory ) = PlaylistImpl( rawPlaylist.playlistInfo.playlistUid, - Name.Known.from(rawPlaylist.playlistInfo.name, null, musicSettings), + nameFactory.parse(rawPlaylist.playlistInfo.name, null), rawPlaylist.songs.mapNotNull { deviceLibrary.findSong(it.songUid) }) } } 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 06de6d64f..faae9594b 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 @@ -26,6 +26,7 @@ import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.device.DeviceLibrary +import org.oxycblt.auxio.music.info.Name import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logE @@ -144,7 +145,9 @@ constructor(private val playlistDao: PlaylistDao, private val musicSettings: Mus UserLibrary.Factory { override suspend fun query() = try { - playlistDao.readRawPlaylists() + val rawPlaylists = playlistDao.readRawPlaylists() + logD("Successfully read ${rawPlaylists.size} playlists") + rawPlaylists } catch (e: Exception) { logE("Unable to read playlists: $e") listOf() @@ -154,11 +157,10 @@ constructor(private val playlistDao: PlaylistDao, private val musicSettings: Mus rawPlaylists: List, deviceLibrary: DeviceLibrary ): MutableUserLibrary { - logD("Successfully read ${rawPlaylists.size} playlists") - // Convert the database playlist information to actual usable playlists. + val nameFactory = Name.Known.Factory.from(musicSettings) val playlistMap = mutableMapOf() for (rawPlaylist in rawPlaylists) { - val playlistImpl = PlaylistImpl.fromRaw(rawPlaylist, deviceLibrary, musicSettings) + val playlistImpl = PlaylistImpl.fromRaw(rawPlaylist, deviceLibrary, nameFactory) playlistMap[playlistImpl.uid] = playlistImpl } return UserLibraryImpl(playlistDao, playlistMap, musicSettings) @@ -184,7 +186,7 @@ private class UserLibraryImpl( override fun findPlaylist(name: String) = playlistMap.values.find { it.name.raw == name } override suspend fun createPlaylist(name: String, songs: List): Playlist? { - val playlistImpl = PlaylistImpl.from(name, songs, musicSettings) + val playlistImpl = PlaylistImpl.from(name, songs, Name.Known.Factory.from(musicSettings)) synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl } val rawPlaylist = RawPlaylist( @@ -207,7 +209,9 @@ private class UserLibraryImpl( val playlistImpl = synchronized(this) { requireNotNull(playlistMap[playlist.uid]) { "Cannot rename invalid playlist" } - .also { playlistMap[it.uid] = it.edit(name, musicSettings) } + .also { + playlistMap[it.uid] = it.edit(name, Name.Known.Factory.from(musicSettings)) + } } return try { 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 d3e7b8cda..1910b1a01 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 @@ -338,8 +338,7 @@ constructor( song, object : BitmapProvider.Target { override fun onCompleted(bitmap: Bitmap?) { - this@MediaSessionComponent.logD( - "Bitmap loaded, applying media session and posting notification") + logD("Bitmap loaded, applying media session and posting notification") builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, bitmap) builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmap) val metadata = builder.build() 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 c131238e0..15c8cf0eb 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 @@ -166,7 +166,7 @@ class PlaybackService : } ContextCompat.registerReceiver( - this, systemReceiver, intentFilter, ContextCompat.RECEIVER_NOT_EXPORTED) + this, systemReceiver, intentFilter, ContextCompat.RECEIVER_EXPORTED) logD("Service created") } diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt index 48a436730..da74d66a2 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt @@ -119,7 +119,7 @@ class SearchFragment : ListFragment() { if (!launchedKeyboard) { // Auto-open the keyboard when this view is shown - this@SearchFragment.logD("Keyboard is not shown yet") + logD("Keyboard is not shown yet") showKeyboard(this) launchedKeyboard = true } diff --git a/app/src/main/java/org/oxycblt/auxio/util/LogUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/LogUtil.kt index f7418a61e..bc1197af4 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/LogUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/LogUtil.kt @@ -18,27 +18,24 @@ package org.oxycblt.auxio.util -import android.util.Log import org.oxycblt.auxio.BuildConfig - -// Shortcut functions for logging. -// Yes, I know timber exists but this does what I need. +import timber.log.Timber /** * Log an object to the debug channel. Automatically handles tags. * * @param obj The object to log. */ -fun Any.logD(obj: Any?) = logD("$obj") +inline fun logD(obj: Any?) = logD("$obj") /** * Log a string message to the debug channel. Automatically handles tags. * * @param msg The message to log. */ -fun Any.logD(msg: String) { +inline fun logD(msg: String) { if (BuildConfig.DEBUG && !copyleftNotice()) { - Log.d(autoTag, msg) + Timber.d(msg) } } @@ -47,31 +44,24 @@ fun Any.logD(msg: String) { * * @param msg The message to log. */ -fun Any.logW(msg: String) = Log.w(autoTag, msg) +inline fun logW(msg: String) = Timber.w(msg) /** * Log a string message to the error channel. Automatically handles tags. * * @param msg The message to log. */ -fun Any.logE(msg: String) = Log.e(autoTag, msg) - -/** - * The LogCat-suitable tag for this string. Consists of the object's name, or "Anonymous Object" if - * the object does not exist. - */ -private val Any.autoTag: String - get() = "Auxio.${this::class.simpleName ?: "Anonymous Object"}" +inline fun logE(msg: String) = Timber.e(msg) /** * Please don't plagiarize Auxio! You are free to remove this as long as you continue to keep your * source open. */ @Suppress("KotlinConstantConditions") -private fun copyleftNotice(): Boolean { +fun copyleftNotice(): Boolean { if (BuildConfig.APPLICATION_ID != "org.oxycblt.auxio" && BuildConfig.APPLICATION_ID != "org.oxycblt.auxio.debug") { - Log.d( + Timber.d( "Auxio Project", "Friendly reminder: Auxio is licensed under the " + "GPLv3 and all derivative apps must be made open source!") diff --git a/app/src/main/res/layout-h480dp/fragment_playback_panel.xml b/app/src/main/res/layout-h480dp/fragment_playback_panel.xml index 263bf2a7d..e68de3423 100644 --- a/app/src/main/res/layout-h480dp/fragment_playback_panel.xml +++ b/app/src/main/res/layout-h480dp/fragment_playback_panel.xml @@ -154,4 +154,4 @@ - + \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp/fragment_playback_panel.xml b/app/src/main/res/layout-sw600dp/fragment_playback_panel.xml index b46562fa2..eecb65e6e 100644 --- a/app/src/main/res/layout-sw600dp/fragment_playback_panel.xml +++ b/app/src/main/res/layout-sw600dp/fragment_playback_panel.xml @@ -157,4 +157,4 @@ - + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_playback_panel.xml b/app/src/main/res/layout/fragment_playback_panel.xml index 74f106903..857cb7545 100644 --- a/app/src/main/res/layout/fragment_playback_panel.xml +++ b/app/src/main/res/layout/fragment_playback_panel.xml @@ -159,4 +159,4 @@ - + \ No newline at end of file diff --git a/app/src/main/res/navigation/inner.xml b/app/src/main/res/navigation/inner.xml index a974b3360..43c0d52c3 100644 --- a/app/src/main/res/navigation/inner.xml +++ b/app/src/main/res/navigation/inner.xml @@ -379,6 +379,9 @@ + diff --git a/app/src/main/res/values-ar-rIQ/strings.xml b/app/src/main/res/values-ar-rIQ/strings.xml index 97794174f..011240291 100644 --- a/app/src/main/res/values-ar-rIQ/strings.xml +++ b/app/src/main/res/values-ar-rIQ/strings.xml @@ -1,7 +1,7 @@ - مشغل موسيقى بسيط ومعقول للأندرويد + مشغل موسيقى بسيط ومعقول للأندرويد. عرض وتحكم بشتغيل الموسيقى إعادة المحاولة @@ -40,7 +40,7 @@ الإصدار عرض على الكود في Github التراخيص - تمت برمجة التطبيق من قبل OxygenCobalt + تمت برمجة التطبيق من قبل الكساندر كابيهارت الإعدادات المظهر @@ -192,4 +192,27 @@ مزيج Wiki أغنية + أتجاه + أختيار + قوائم التشغيل + قائمة التشغيل + تم خلق قائمة التشغيل + المزيد + حذف + تم النسخ + إضافة إلى قائمة التشغيل + مشاركة + تعديل + إعادة التسمية + تمت الإضافة إلى قائمة التشغيل + رتب حسب + مشاهدة + حذف قائمة التشغيل؟ + تم حذف قائمة التشغيل + تقرير + قائمة تشغيل جديدة + معلومات خاطئة + تم إعادة تسمية قائمة التشغيل + إعادة تسمية قائمة التشغيل + يظهر على \ No newline at end of file diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index 106d70d59..0084fbd04 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -300,4 +300,8 @@ Напрамак Абярыце малюнак Абярыце + Дадаткова + Скапіравана + Інфармацыя пра памылку + Справаздача пра памылку \ No newline at end of file diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 9e55b50e3..3a2ef1255 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -311,4 +311,8 @@ Seřadit podle Výběr obrázku Výběr + Další + Informace o chybě + Zkopírovat + Nahlásit \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index d30ef1144..35e2abb16 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -300,4 +300,10 @@ Lied selbst spielen Richtung Sortieren nach + Auswahl-Bild + Auswahl + Mehr + Kopiert + Melden + Fehlerinformation \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 0712780dd..a4a109704 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -306,4 +306,8 @@ Dirección Selección de imágenes Selección + Más + Información sobre el error + Copiado + Informar \ No newline at end of file diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 55677cbf1..262cfde7b 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -271,4 +271,7 @@ Tyhjennä tunnistevälimuisti ja lataa musiikkikirjasto kokonaan uudelleen (hitaampi mutta kattavampi) Kappale Näytä + Lisää + Kopioitu + Ilmoita virheestä \ 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 b063a1730..742007eb0 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -242,7 +242,7 @@ Ajouter à la liste de lecture Créer une nouvelle liste de lecture Audio Matroska - Artistes chargés&nbsp;: %d + Artistes chargés : %d Rembobiner avant de revenir en arrière Image d\'artiste pour %s Aucune piste @@ -253,8 +253,8 @@ Renommer Impossible d\'effacer l\'état Modifier le mode de répétition - Albums chargés&nbsp;: %d - Durée totale&nbsp;: %s + Albums chargés : %d + Durée totale : %s Effacer la requête de recherche Image de la liste de lecture pour %s Disque %d @@ -288,7 +288,7 @@ Impossible de sauvegarder l\'état Aucune chanson Modification de %s - Genres chargés&nbsp;: %d + Genres chargés : %d Image de genre pour %s Codec audio gratuit sans perte (FLAC) %d sélectionnés @@ -300,4 +300,12 @@ Chanson Voir Jouer la chanson par elle-même + Image de sélection + Trier par + Direction + Sélection + En savoir plus + Copié + Signaler + Info sur l\'erreur \ 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 97ffbd5ca..0384924fd 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -299,4 +299,10 @@ इसी गीत को चलाएं दिशा के अनुसार क्रमबद्ध करें + संग्रह + चयन छवि + त्रुटि की जानकारी + रिपोर्ट करें + कापी किया गया + और \ 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 fecd2b6f2..22bab4d29 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -297,4 +297,8 @@ Smjer Slika odabira Odabir + Više + Podaci greške + Prijavi grešku + Kopirano \ No newline at end of file diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index cec9b922a..c92aa6abf 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -301,4 +301,8 @@ Rendezés Kiválasztás Kép kiválasztás + További + Másolva + Jelentés + Hiba információ \ No newline at end of file diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 21f166a7e..3b2486eb4 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -211,4 +211,26 @@ Pustaka Pemutaran Ampersand (&) + Kompilasi + Kompilasi remix + EP + EP Live + Kompilasi + Kaset campuran + Lainnya + Soundtrack + Album live + + %d artis + + Single Live + EP + Kaset campuran + Single remix + Lagu + Single + Soundtrek + Kompilasi live + EP Remix + Single \ No newline at end of file diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index e85f5a07e..d74cc9319 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -176,12 +176,12 @@ לא ניתן לשמור את המצב ‏ Auxio צריך הרשאות על מנת לקרוא את ספריית המוזיקה שלך פתיחת התור - סך הכל משך: %s + משך כולל: %s רשימת השמעה %d אומנים טעונים: %d שירים טעונים: %d אלבומים טעונים: %d - סוגות טעונות: %d + ז\'אנרים טעונים: %d המצב נוקה ספריה שמירת מצב הנגינה הנוכחי כעת @@ -197,15 +197,15 @@ תמונת אומן עבור %s יצירת תמונה עבור %s אומן לא ידוע - סוגה לא ידועה + ז\'אנר לא ידוע אין תאריך אין רצועה - אך מוזיקה אינה מתנגנת + מוזיקה לא מתנגנת כחול כחול עמוק אפור דינמי - המוזיקה שלך בטעינה (‎%1$d/%2$d)… + המוזיקה שלך בטעינה… (‎%1$d/%2$d) דיסק %d ניהול המקומות שמהם תיטען מוזיקה אין שירים @@ -215,7 +215,7 @@ אומן אחד שני אומנים - %d אומנים + %d אומנים לכלול רענון מוזיקה @@ -226,12 +226,12 @@ שיר אחד שני שירים - %d שירים + %d שירים אלבום אחד שני אלבומים - %d אלבומים + %d אלבומים שונה שם רשימת ההשמעה רשימת השמעה נמחקה @@ -242,7 +242,7 @@ תמונת רשימת השמעה עבור %s אדום ירוק - נתיב הורה + נתיב ראשי לא ניתן לשחזר את המצב רצועה %d יצירת רשימת השמעה חדשה @@ -257,7 +257,7 @@ אין דיסק ירוק עמוק צהוב - מחיקת %s\? פעולה זו לא ניתן לביטול. + למחוק את %s\? פעולה זו לא ניתן לביטול. שיר מיון חכם הצגה @@ -271,6 +271,34 @@ מוזיקה תיטען רק מהתיקיות שנוספו. מופיע~ה ב- ניגון השיר בלבד - אזהרה: שינוי המגבר לערך חיובי גבוה עלול לגרום לשיאים בחלק מרצועות האודיו + אזהרה: שינוי המגבר לערך חיובי גבוה עלול לגרום לעיוות (דיסטורשן) בחלק מרצועות האודיו. שחזור מצב נגינה + אינדיגו + אודיו MPEG-1 + אודיו MPEG-4 + אודיו Ogg + ציאן + טורקיז + חום + %d נבחרו + התמדה + עוד + בחירה + מידע על השגיאה + דיווח + תמונה נבחרת + קודק אודיו חופשי ללא איבוד נתונים (FLAC) + סגול + סגול עמוק + +%.1f דציבלים (dB) + -%.1f דציבלים (dB) + %d הרץ (Hz) + %d קילוביטים לשנייה (kbps) + מועתק + שחזור מצב הנגינה שנשמר קודם (אם קיים) + אודיו Matroska + קידוד אודיו מתקדם (AAC) + %1$s, %2$s + ליים + %s נערך \ No newline at end of file diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 70aa17ba9..0d9822a7f 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -1,11 +1,11 @@ - 단순하고, 실용적인 안드로이드용 뮤직 플레이어입니다. + 단순하고, 실용적인 안드로이드용 음악 플레이어입니다. 음악 재생 제어 및 상태 확인 - 재시도 - 허가 + 다시 시도 + 허용 장르 아티스트 앨범 @@ -26,14 +26,14 @@ 오름차순 지금 재생 중 재생 - 셔플 + 무작위 재생 모든 곡에서 재생 앨범에서 재생 아티스트에서 재생 대기열 다음 곡 재생 대기열에 추가 - 대기열에 추가됨 + 대기열에 추가했습니다. 아티스트로 이동 앨범으로 이동 상태 저장됨 @@ -46,24 +46,24 @@ 정보 버전 소스 코드 - 라이센스 + 라이선스 Alexander Capehart가 개발 라이브러리 통계 설정 - 보고 느낌 + 모양과 느낌 테마 자동 - 밝음 - 어두움 + 라이트 테마 + 다크 테마 배색 검정 테마 - 어두운 테마에 검정색 사용 + 다크 테마에 검정색 사용 화면 라이브러리 탭 - 라이브러리 탭의 순서 및 표시할 탭 변경 + 라이브러리 탭 순서 및 표시할 탭 변경 둥근 UI 모드 - 기타 UI 요소 가장자리를 둥글게 표시 (앨범 커버도 둥글어짐) + 앨범 커버를 포함한 기타 UI의 가장자리를 둥글게 표시합니다. 알림 동작 사용자 정의 소리 헤드셋 자동 재생 @@ -71,32 +71,32 @@ ReplayGain 계획 트랙 선호 앨범 선호 - 앨법 재생 중인 경우 앨범 선호 + 앨범 재생 중인 경우 앨범 선호 ReplayGain 프리앰프 재생 중에 프리앰프를 적용하여 조정 태그로 조정 태그 없이 조정 - 주의: 프리앰프를 높게 설정하면 일부 소리 트랙이 왜곡될 수 있습니다. - 동작 + 주의: 프리앰프를 높게 설정하면 일부 오디오 트랙이 왜곡될 수 있습니다. + 개인화 라이브러리에서 재생할 때 무작위 재생 기억 - 새로운 곡을 재생할 때 무작위 재생 유지 + 새로운 곡을 재생할 때 무작위 재생 모드 유지 이전 곡으로 가기 전에 되감기 이전 곡으로 건너뛰기 전에 먼저 현재 트랙을 되감기 반복 재생 시 일시 중지 곡이 반복 재생될 때 일시 중지 내용 재생 상태 저장 - 현재 재생 상태를 즉시 저장 + 현재 재생 상태를 지금 저장합니다. 음악 새로고침 - 이미 저장된 태그를 가능한 활용하여 음악 라이브러리를 다시 만들기 + 캐시된 태그를 사용하여 음악 라이브러리를 다시 불러옵니다. 음악 없음 음악 불러오기 실패 - Auxio가 음악 라이브러리를 읽을 수 있는 권한이 필요함 - 이 작업을 처리할 수 있는 앱을 찾지 못함 + 앱에서 음악 라이브러리를 읽을 수 있는 권한이 필요합니다. + 이 작업을 처리할 수 있는 앱을 찾을 수 없습니다. 폴더 없음 - 이 폴더는 지원되지 않음 + 지원하지 않는 폴더입니다. 라이브러리에서 검색… @@ -107,8 +107,8 @@ 반복 방식 변경 무작위 재생 켜기 또는 끄기 모든 곡 무작위 재생 - 이 대기열의 곡 제거 - 이 대기열의 곡 이동 + 이 곡 제거 + 이 곡 이동 이 탭 이동 검색 기록 삭제 폴더 제거 @@ -157,8 +157,8 @@ %d 앨범 MPEG-4 오디오 - 자유 무손실 오디오 코덱 (FLAC) - 이전에 저장된 재생 상태 지우기 (있는 경우) + Free Lossless Audio Codec (FLAC) + 이전에 저장된 재생 상태 초기화 제외 추가한 폴더에서만 음악을 불러옵니다. 곡 속성 @@ -167,34 +167,34 @@ 샘플 속도 전송 속도 크기 - 모두 셔플 + 모두 무작위 재생 재생 중지 Ogg 오디오 - 마트로스카 오디오 + Matroska 오디오 %d Hz DJ믹스 라이브 컴필레이션 리믹스 편집 DJ믹스 이퀄라이저 - 셔플 + 무작위 재생 표시된 항목에서 재생 - 음악 라이브러리를 불러오는 중… + 음악 라이브러리 불러오는 중… 재생 상태 지우기 재생 상태 복원 음악 폴더 음악을 불러오는 위치 관리 추가한 폴더에서 음악을 불러오지 않습니다. 포함 - 다중값 구분자 - 여러 태그 값을 나타낼때의 구분자 설정 + 다중 값 구분 기호 + 태그 값이 여러 개일 때 태그를 구분할 기호를 설정합니다. 콤마 (,) 세미콜론 (;) - 슬래쉬 (/) + 슬래시 (/) 플러스 (+) 앰퍼샌드 (&) MPEG-1 오디오 - 추가된 날짜 + 추가한 날짜 상위 경로 맞춤형 재생 동작 버튼 반복 방식 @@ -208,20 +208,20 @@ 컴필레이션 라이브 형식 - 음악 아닌 것 제외 + 음악이 아닌 항목 제외 %d kbps - 고급 오디오 코딩 (AAC) + Advanced Audio Coding (AAC) 앨범 커버 빠름 고품질 - 이전에 저장된 재생 상태 복원 (있는 경우) - 재생상태를 복원할 수 없음 + 이전에 저장된 재생 상태 복원 + 재생 상태를 복원할 수 없습니다. 음악 라이브러리가 변경될 때마다 새로고침 (고정 알림 필요) 상태 지워짐 - 음악 불러오기 중 - 음악 불러오기 중 - 음악 라이브러리 추적중 + 음악 불러오는 중 + 음악 불러오는 중 + 음악 라이브러리 모니터링 중 상태 복원됨 EP 앨범 EP 앨범 @@ -234,72 +234,76 @@ 믹스테이프 리믹스 자동 새로고침 - 처리방식 + 모드 음악 라이브러리를 불러오는 중… (%1$d/%2$d) 장르 - 경고: 이 설정을 사용하면 일부 태그가 여러 값을 갖는 것으로 잘못 해석될 수 있습니다. 구분자로 읽히지 않도록 하려면 해당 구분자 앞에 백슬래시 (\\)를 붙입니다. + 경고: 이 설정을 사용하면 몇몇 태그가 다중 값을 가진 것으로 잘못 나타날 수 있습니다. 태그에서 구분 기호 앞에 백슬래시(\\)를 붙이면 구분 기호로 인식하지 않습니다. 항목 세부 정보에서 재생할 때 - 음악 라이브러리의 변경사항을 추적하는 중… + 음악 라이브러리 변경 사항 모니터링 중… 다음 곡으로 건너뛰기 - 팟캐스트와 같이 음악이 아닌 소리 파일 무시 - 공동작업자 숨기기 + 팟캐스트 등 음악이 아닌 오디오 파일 무시 + 공동 작업자 숨기기 앨범에 등장하는 아티스트만 표시 (자세히 태그된 라이브러리에 최적화) - 재생상태를 지울 수 없음 - 재생상태를 저장할 수 없음 + 재생 상태를 지울 수 없습니다. + 재생 상태를 저장할 수 없습니다. 음악 재탐색 %d 아티스트 - 태그 정보를 지우고 음악 라이브러리를 재생성함(느림, 더 완전함) + 태그 캐시를 지우고 음악 라이브러리를 처음부터 다시 생성합니다. 느리지만 더 완벽한 방식입니다. %d 선택됨 재설정 위키 장르에서 재생 %1$s, %2$s - 리플레이게인 + ReplayGain 사운드 및 재생 동작 구성 재생 폴더 - 앱의 테마 및 색상 변경 + 앱 테마 및 색상 변경 음악 라이브러리 이미지 - 음악 및 이미지 불러오기 방법 제어 + 음악 및 이미지 불러오기 방식 설정 지속 동작 - UI 제어 및 동작 커스텀 + UI 제어 및 동작 사용자 정의 내림차순 - 재생목록 - 재생목록 + 재생 목록 + 재생 목록 %s의 재생 목록 이미지 - 정렬할 때 기사 무시 - 이름으로 정렬할 때 \"the\"와 같은 단어 무시(영어 음악에서 가장 잘 작동함) + 적응형 정렬 + 정렬할 때 숫자나 \"the\"와 같은 단어를 무시합니다. 태그가 영어로 되어 있을 때 가장 잘 작동합니다. 새 재생 목록 만들기 - 새 재생목록 - 재생목록에 추가 - 생성된 재생목록 - 재생목록에 추가됨 - 재생목록 %d + 새 재생 목록 + 재생 목록에 추가 + 재생 목록을 만들었습니다. + 재생 목록에 추가했습니다. + 재생 목록 %d 노래 없음 삭제 - %s를 삭제하시겠습니까\? 이 취소 할 수 없습니다. + %s 항목을 삭제하시겠습니까\? 이 작업은 취소할 수 없습니다. 이름 바꾸기 - 재생목록 이름 바꾸기 - 재생목록을 삭제하시겠습니까\? - 편집하다 + 재생 목록 이름 바꾸기 + 재생 목록을 삭제하시겠습니까\? + 편집 에 나타납니다 - 공유하다 - 재생목록의 이름이 변경됨 + 공유 + 재생 목록의 이름을 바꿨습니다. 디스크 없음 - 재생목록이 삭제되었습니다 - %s 수정 중 + 재생 목록을 삭제했습니다. + %s 편집 중 노래 - 보다 - 포스 스퀘어 앨범 커버 - 모든 앨범 표지를 1:1 가로세로 비율로 자르기 + 보기 + 정사각형 앨범 커버 강제 + 모든 앨범 커버를 가로세로 1:1 비율로 자릅니다. 노래 따로 재생 방향 정렬 기준 선택 이미지 선택 + 더 보기 + 복사했습니다. + 오류 보고 + 오류 정보 \ No newline at end of file diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index cfcbccdcf..0b2138093 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -2,10 +2,10 @@ Dainos Visos dainos - Ieškoti + Paieška Filtruoti Visos - Rūšiuoti + Rūšiavimas Pavadinimas Metai Trukmė @@ -34,7 +34,7 @@ Groti Licencijos Maišyti - Pridėta į eilę + Pridėtas į eilę Dainų ypatybės Failo pavadinimas Išsaugoti @@ -47,7 +47,7 @@ Tema Naudoti grynai juodą tamsią temą Paprastas, racionalus Android muzikos grotuvas. - Muzika kraunama + Muzikos pakraunimas Peržiūrėk ir valdyk muzikos grojimą Žanrai Pakartoti @@ -72,7 +72,7 @@ Albumo viršelis Giliai violetinė Stebėjimas muzikos biblioteka - Stebima tavo muzikos biblioteko dėl pakeitimų… + Stebimas tavo muzikos biblioteka dėl pakeitimų… Maišyti Maišyti viską Atkurta būsena @@ -123,7 +123,7 @@ Gyvai Visada pradėti groti, kai ausinės yra prijungtos (gali neveikti visuose įrenginiuose) Ogg garsas - Sukūrė Alexanderis Capehartas + Sukūrė Alexanderis Capehartas (angl. Alexander Capehart) Pageidauti takelį Jokių aplankų Šis aplankas nepalaikomas @@ -159,7 +159,7 @@ Kai grojant iš elemento detalių Pašalinti aplanką Žanras - Ieškok savo bibliotekoje… + Ieškoti savo bibliotekoje… Ekvalaizeris Režimas Automatinis įkrovimas @@ -171,7 +171,7 @@ Kartojimo režimas Atidaryti eilę Išvalyti paieškos paraišką - Muzika nebus įkeliama iš pridėtų aplankų, kurių tu pridėsi. + Muzika nebus kraunama iš pridėtų aplankų, kurių tu pridėsi. Įtraukti Pašalinti šią dainą Groti iš visų dainų @@ -180,17 +180,17 @@ Groti iš atlikėjo Išvalyta būsena Neįtraukti - Muzika bus įkeliama iš aplankų, kurių tu pridėsi. + Muzika bus kraunama iš aplankų, kurių tu pridėsi. %d Hz Perkrauti muzikos biblioteką, kai ji pasikeičia (reikia nuolatinio pranešimo) - Įkeltos dainos: %d - Įkeltos žanrai: %d - Įkeltos albumai: %d - Įkeltos atlikėjai: %d - Kraunamas tavo muzikos biblioteka… (%1$d/%2$d) + Pakrautos dainos: %d + Pakrautos žanros: %d + Pakrauti albumai: %d + Pakrauti atlikėjai: %d + Kraunama tavo muzikos biblioteka… (%1$d/%2$d) Maišyti visas dainas Personalizuotas - Įspėjimas: Keičiant išankstinį stiprintuvą į didelę teigiamą vertę, kai kuriuose garso takeliuose gali atsirasti tarpų. + Įspėjimas: keičiant išankstinį stiprintuvą į didelę teigiamą vertę, kai kuriuose garso takeliuose gali atsirasti tarpų. Albumo viršelis %s Atlikėjo vaizdas %s Nėra grojančio muzikos @@ -214,7 +214,7 @@ DJ miksas Gyvai kompiliacija Remikso kompiliacija - Pagrindinis aplankas + Pirminis kelias Išvalyti anksčiau išsaugotą grojimo būseną (jei yra) Daugiareikšmiai separatoriai Pasvirasis brūkšnys (/) @@ -228,7 +228,7 @@ Konfigūruoti simbolius, kurie nurodo kelias žymių reikšmes Kablelis (,) Reguliavimas be žymų - Įspėjimas: Naudojant šį nustatymą, kai kurios žymos gali būti neteisingai interpretuojamos kaip turinčios kelias reikšmes. Tai galima išspręsti prieš nepageidaujamus skiriamuosius ženklus naudojant atgalinį brūkšnį (\\). + Įspėjimas: naudojant šį nustatymą, kai kurios žymos gali būti neteisingai interpretuojamos kaip turinčios kelias reikšmes. Tai galima išspręsti prieš nepageidaujamus skiriamuosius ženklus su agalinių brūkšniu (\\). Kabliataškis (;) Aukštos kokybės Atkurti grojimo būseną @@ -245,6 +245,7 @@ %d atlikėjas (-a) %d atlikėjai %d atlikėjų + %d atlikėjų Perskenuoti muziką Išvalyti žymių talpyklą ir pilnai perkrauti muzikos biblioteką (lėčiau, bet labiau išbaigta) @@ -297,4 +298,10 @@ Groti dainą pačią Rūšiuoti pagal Kryptis + Pasirinkimo vaizdas + Pasirinkimas + Klaidos informacija + Nukopijuota + Daugiau + Pranešti \ 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 175c77cdb..1d385c7bc 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -93,11 +93,11 @@ ਐਪ ਦਾ ਥੀਮ ਅਤੇ ਰੰਗ ਬਦਲੋ ਥੀਮ ਸਵੈਚਾਲਿਤ - ਹਲਕਾ + ਸਫ਼ੈਦ ਗੂੜ੍ਹਾ ਰੰਗ ਸਕੀਮ ਕਾਲ੍ਹਾ ਥੀਮ - ਇੱਕ ਸ਼ੁੱਧ-ਕਾਲ੍ਹਾ ਗੂੜ੍ਹਾ ਥੀਮ ਵਰਤੋ + ਸ਼ਾਹ-ਕਾਲ਼ਾ ਥੀਮ ਵਰਤੋ ਗੋਲ ਮੋਡ ਵਾਧੂ UI ਤੱਤਾਂ \'ਤੇ ਗੋਲ ਕੋਨਿਆਂ ਨੂੰ ਸਮਰੱਥ ਬਣਾਓ (ਗੋਲਾਕਾਰ ਕਰਨ ਲਈ ਐਲਬਮ ਕਵਰਾਂ ਦੀ ਲੋੜ ਹੁੰਦੀ ਹੈ ) ਵਿਅਕਤੀਗਤ ਬਣਾਓ @@ -292,4 +292,10 @@ ਇਸੇ ਗੀਤ ਨੂੰ ਚਲਾਓ ਸੌਰਟ ਕਰੋ ਦਿਸ਼ਾ + ਚੋਣ + ਚੋਣ ਚਿੱਤਰ + ਹੋਰ + ਤਰੁੱਟੀ ਦੀ ਜਾਣਕਾਰੀ + ਕਾਪੀ ਕੀਤਾ ਗਿਆ + ਰਿਪੋਰਟ ਕਰੋ \ No newline at end of file diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index ef6191ce3..6716a415a 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -11,12 +11,12 @@ Pesquisar Filtro Tudo - Classificar + Organizar Reproduzir Aleatório Tocando agora Fila - Reproduzir próxima + Reproduzir a seguir Adicionar à fila Adicionada à fila Ir para o artista @@ -145,7 +145,7 @@ Áudio MPEG-4 Áudio Ogg Áudio Matroska - Codificação de Audio Avançada (AAC) + Advanced Audio Coding (AAC) Free Lossless Audio Codec (FLAC) Mover esta música da fila Dinâmico @@ -273,4 +273,27 @@ Descendente Ignorar artigos ao classificar Ignore palavras como \"the\" ao classificar por nome (funciona melhor com músicas em inglês) + Playlists + Playlist %d + Playlist + Playlist criada + Mais + Apagar + Copiado + Adicionar à playlist + Compartilhar + Editar + Renomear + Adicionada à playlist + Editando %s + Organizar por + Música + Apagar playlist\? + Criar uma nova playlist + Playlist deletada + Nova playlist + Playlist renomeada + Renomear playlist + Aparece em + Apagar %s\? Esta ação não pode ser desfeita. \ No newline at end of file diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index 3a297776d..e02dbbe6c 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -3,19 +3,19 @@ Tentar novamente Permitir - Gêneros + Géneros Artistas Álbuns Músicas Todas as músicas - Pesquisar + Procurar Filtrar Tudo - Classificação + Organizar Ascendente Reproduzir - Embaralhar - Tocando agora + Misturar + A tocar agora Fila Reproduzir a próxima Adicionar à fila @@ -34,10 +34,10 @@ Claro Escuro Automático - Cor de realce + Esquema de cores Áudio - Comportamento - Memorizar aleatorização + Personalizar + Memorizar musica misturada Nenhuma música encontrada @@ -79,19 +79,19 @@ Áudio MPEG-4 Artistas carregados: %d Duração total: %s - Falha no carregamento da música + Falha ao carregar música Nome - Prefira o álbum se estiver tocando - Nenhuma aplicação encontrada que possa lidar com esta tarefa + Prefira o álbum se estiver a tocar + Nenhuma aplicação encontrada que possa executar esta tarefa Ciano Contagem de músicas Formato Estatísticas da biblioteca Capa do álbum - Ano + Data Rápido Qualidade alta - Ação da barra de reprodução personalizada + Personalizar a barra de reprodução Modo de repetição Reproduzir do artista Pausar na repetição @@ -100,29 +100,29 @@ Esta pasta não é compatível Mover esta música da fila Remover pasta - Compilações de remix + Mistura de compilações Compilação ao vivo Disco Faixa Taxa de bits - Pular para o próximo + Avançar para o próximo Aviso: Alterar o pré-amplificador para um valor positivo alto pode resultar em picos em algumas faixas de áudio. - Ajuste com etiquetas + Ajustar com etiquetas Barra (/) Mais (+) Áudio Ogg Data adicionada Taxa de amostragem - Gravar + Salvar Separadores multi-valor Nome do ficheiro Tamanho - Ver propriedades + Propriedades Propriedades da música OK Adicionar Estado salvo - Estado liberado + Estado limpo Tema preto Limpar consulta de pesquisa Imagem de gênero para %s @@ -139,8 +139,8 @@ Ao vivo Duração Cancelar - A carregar a sua biblioteca de músicas… - Gira de onde a música deve ser carregada + A carregar biblioteca de músicas… + Configurar onde a música deve ser carregada Gênero Mantenha a reprodução aleatória ao reproduzir uma nova música Pular para a próxima música @@ -152,7 +152,7 @@ Excluir A música não será carregada das pastas que adicionar. Incluir - A música somente será carregada das pastas que adicionar. + A música será somente carregada das pastas que adicionar. Excluir não-música Ignorar ficheiros de áudio que não são música, tal como podcasts Configurar caracteres que denotam múltiplos valores de etiqueta @@ -167,30 +167,30 @@ Dinâmico Disco %d Capa do álbum para %s - Ajuste sem etiquetas + Ajustar sem etiquetas Conteúdo Gêneros carregados: %d A carregar música A carregar música - A monitorar a biblioteca de música + A monitorizar a biblioteca de música Equalizador Um reprodutor de música simples e racional para Android. Estado restaurado - Exibição + Mostrar Abas da biblioteca Altere a visibilidade e a ordem das abas da biblioteca Capas de álbuns Desligado Modo redondo - Usar ação de notificação alternativa - Reprodução automática do fone de ouvido - Sempre comece a tocar quando um fone de ouvido estiver conectado (pode não funcionar em todos os aparelhos) + Personalizar notificações + Reprodução automática dos auscultadores + Iniciar música quando os auscultadores forem conectados (pode não funcionar em todos os aparelhos) Estratégia do ganho de repetição Preferir álbum O pré-amplificador é aplicado ao ajuste existente durante a reprodução Reproduzir de todas as músicas - Pausa quando uma música se repete - Limpe o estado de reprodução salvo anteriormente (se houver) + Pausar quando uma música é repetida + Limpar o estado de reprodução salvo anteriormente (se houver) Restaurar o estado de reprodução Restaurar o estado de reprodução salvo anteriormente (se houver) Ativar ou desativar a reprodução aleatória @@ -207,8 +207,8 @@ Mixtape Remixes Artista - Gravar estado de reprodução - Salve o estado de reprodução atual agora + Gravar estado da reprodução + Salvar o estado de reprodução atual Limpar estado de reprodução Álbum ao vivo -%.1f dB @@ -220,20 +220,20 @@ Caminho principal Ativar cantos arredondados em elementos adicionais da interface do utilizador (requer que as capas dos álbuns sejam arredondadas) %d Selecionadas - Mixes - Mix + Misturas DJ + DJ Mix Aleatório - Ocultar artistas colaboradores + Ocultar colaboradores Limpa os metadados em cache e recarrega totalmente a biblioteca de música (lento, porém mais completo) Álbum de Remix Single ao vivo Single remix - Monitorando alterações na sua biblioteca de músicas… + A Monitorizar alterações na sua biblioteca de músicas… Recarrega a biblioteca de músicas sempre que ela mudar (requer notificação fixa) - Redefinir + Repor Wiki - Visualize e controle a reprodução de música - Use um tema preto + Vêr e controlar a reprodução da música + Utilizar tema preto puro Mostrar apenas artistas que foram creditados diretamente no álbum (funciona melhor em músicas com metadados completos) Preferir faixa Pré-amplificação da normalização de volume @@ -244,10 +244,10 @@ %1$s, %2$s Não foi possível limpar a lista Não foi possível gravar a lista - Re-escanear músicas + Procurar músicas novamente Nenhuma lista pode ser restaurada Ícone do Auxio - Aleatorizar tudo + Misturar tudo Ao tocar da biblioteca Singles Single @@ -257,20 +257,55 @@ %d artistas %d artistas - Equalização de volume ReplayGain + Configurar ganho de repetição Descendente - Mude o tema e as cores do app - Personalize os controles e o comportamento da interface do usuário - Controle como a música e as imagens são carregadas + Mudar o tema e cores da app + Personalize os controlos e o comportamento do interface do utilizador + Controlar como a música e as imagens são carregadas Música Imagens - Configurar som e comportamento de reprodução + Configurar o som e comportamento da reprodução Reprodução Pastas Biblioteca - Estado de reprodução + Estado da reprodução E comercial (&) Comportamento - Ignorar artigos ao classificar + Classificação inteligente Ignore palavras como \"the\" ao classificar por nome (funciona melhor com músicas em inglês) + Direção + Seleção de imagem + Seleção + Tocar música sozinha + Listas de reprodução + Lista de reprodução %d + Lista de reprodução + Lista de reprodução criada + Mais + Imagem da lista de reprodução de %s + Eliminar + Nenhum disco + Copiado + Adicionar à lista de reprodução + Partilhar + Editar + Renomear + Adicionado à lista de reprodução + Nenhuma música + Recortar à capa dos álbuns numa proporção de 1:1 + A editar %s + Ordenar por + Visualizar + Música + Eliminar lista de reprodução + Criar nova lista de reprodução + Lista de reprodução eliminada + Relatório + Nova lista de reprodução + Informações de erro + Forçar capas em formato quadrado + Lista de reprodução renomeada + Renomear lista de reprodução + Excluir %s\? Não pode ser desfeito. + Só aparecer \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml new file mode 100644 index 000000000..3a0906840 --- /dev/null +++ b/app/src/main/res/values-pt/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 4351fd565..e892e2739 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -157,4 +157,38 @@ Redă din album În timpul redării de la detaliile articolului Comportament + Listă de redare nouă + Ignoră fișiere audio care nu sunt muzică, precum podcasturi + Plus (+) + Melodie + Listă de redare creată + Șterge + Ascunde colaboratori + Oprit + Taie toate coperțile de album la raportul de aspect 1:1 + Sortare inteligentă + Redenumiți lista da redare + Șterge lista de redare\? + Redenumiți + Controlează cum muzica și imaginile sunt încărcate + Sortează după + Sortare corectă pentru nume care incep cu numere sau cuvinte precum \"the\" (funcționează cel mai bine cu melodii în limba engleză) + Forțează coperți de album pătrate + Rapid + Calitate mare + Punct și virgulă (;) + Editează + Exclude non-muzică + Adaugat către lista de redare + Reîncărcare automată + Virgulă (,) + Reîncărcați biblioteca de muzică oricând se schimbă (Necesită notificare persistentă) + Imagini + Apare în + Partajați + Listă de redare redenumită + Listă de redare ștearsă + Coperți de album + Adaugă către listă de redare + Direcție \ 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 6e93fb54f..2dc979122 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -309,4 +309,8 @@ Направление Выберите Выберите изображение + Дополнительно + Информация об ошибке + Отчёт об ошибке + Скопировано \ No newline at end of file diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml new file mode 100644 index 000000000..5d8da013d --- /dev/null +++ b/app/src/main/res/values-sl/strings.xml @@ -0,0 +1,307 @@ + + + Siva + Pametno sortiranje + Zbirke + Albumi + Zbirka remiksov + Počisti stanje predvajanja + Pojdi na album + Slika izvajalca za %s + Smer + Dodano v čakalno vrsto + Svetlo + Remiks album + Wiki + Samodejno + Slika izbire + Izbira + Preskoči na naslednjo pesem + Ime + Črna tema + Prikaži samo izvajalce, ki so neposredno navedeni na albumu (najbolje deluje v dobro označenih knjižnicah) + Zavihki knjižnice + Temna vijolična + Predvajaj pesem samostojno + Prednost albumu + Predvajaj iz prikazanega elementa + Samodejno ponovno nalaganje + Ni mogoče počistiti stanja + Podaljšane + Lastnosti pesmi + Spremenite način ponavljanja + Oranžna + Dodaj + Naključno predvajanje + DJ Miks + Prednost pesmi + Glasba ni v predavanju + Žanri + Preprost, racionalen predvajalnik glasbe za Android. + Nalaganje vaše glasbene knjižnice… + Previj nazaj pred skokom na prejšnjo pesem + %d kbps + Spremljanje vaše glasbene knjižnice za spremembe… + Prednost albumu če se album predvaja + Seznami predvajanja + Išči v knjižnici… + Ko se predvaja iz podrobnosti elementa + Ponovno naloži glasbo + Remiksi + Shrani stanje predvajanja + Opozorilo: Sprememba pred-ojačevalca na visoko pozitivno vrednost lahko privede do preseganja na nekaterih avdio posnetkih. + Ni datuma + Ponovno naloži glasbeno knjižnico, uporabi predpomnjene oznake, kadar je mogoče + Najdena ni bila nobena aplikacija, ki bi lahko opravila to nalogo + Prekliči + Vključi + Seznam predvajanja %d + Shrani trenutno stanje predvajanja zdaj + Preskoči na zadnjo pesem + Ponovno naloži glasbeno knjižnico vsakič, ko se zazna sprememba (zahteva vztrajno obvestilo) + Pot do datoteke + + %d pesem + %d pesmi + %d pesmi + %d pesmi + + Podaljšano v živo + Seznam predvajanja + Zbirka + Skrij soustvarjalce + Obdrži naključno predvajanje pri predvajanju nove pesmi + Obnašanje + Izklopljeno + MPEG-4 Audio + Shrani + Odpri čakalno vrsto + Mešanice + Izvajalec + Pravilno razvrsti imena, ki se začnejo z številkami ali besedami, kot so \'the\' (najbolje deluje z angleško glasbo) + Ime datoteke + Zelenkasto modra + Vztrajnost + Premešaj vse pesmi + Seznam predavanja ustvarjen + Celoten čas predvajanja: %s + Ni mogoče shraniti stanja + Pavza ob ponavljanju + Mape za glasbo + Zapomni si naključno predvajanje + Pojdi na izvajalca + Naloženih pesmi: %d + Premakni to pesem + Spremljanje glasbene knjižnice + Pokaži več + Ciano modra + Barvna shema + Slika seznama predvajanja za %s + Odstrani + Previj nazaj preden se preskoči nazaj + Naloženih žanrov: %d + Se predvaja + Odstrani to pesem + Stanje predvajanja shranjeno + Ni diska + Išči + Vedno začnite predvajati, ko se slušalke priključijo (morda ne deluje na vseh napravah) + Glasbene podlage + Premešaj vse + Dodaj v čakalno vrsto + Pred-ojačevalnik ReplayGain + MPEG-1 Audio + Ni mogoče obnoviti stanja + Spremenite temo in barve aplikacije + Poskusi znova + Prilagodi zvok in obnašanje predvajanja + Nadzorujte kako se glasba in slike nalagajo + Izgled in občutek + Izključi + Matroska Audio + Začasna prekinitev ob ponavljanju + Predvajaj + Nalaganje glasbe + Ni najdenih pesmi + Datum + Izprazni predpomnilnik oznak in popolnoma ponovno naloži glasbeno knjižnico (počasneje, vendar bolj popolno) + Pred-ojačevalec se uporablja na obstoječi prilagoditvi med predvajanjem + Predvajaj iz albuma + Glasba + Ta mapa ni podprta + Obnovi prej shranjeno stanje predvajanja (če obstaja) + Razvil Alexander Capehart + Odstrani mapo + Kopirano + Nalaganje glasbe ni uspelo + Album + Ko se predvaja iz knjižnice + Visoka kvaliteta + Prilagoditev brez oznak + Dodaj na seznam predvajanja + Datum vnosa + Deli + Album v živo + Uredi + Naslovnica albuma + Preimenuj + Plus (+) + Stanje predvajanja obnovljeno + %d Izbrano + Neznan izvajatelj + Slike + + %d izvajalec + %d izvajalca + %d izvajalci + %d izvajalcev + + Stanje predvajanja počiščeno + Prikaz + %1$s, %2$s + Ogg Audio + Vse + Poševnica (/) + Dodano na seznam predvajanja + Singl v živo + Ni pesmi + Podaljšano + Pesmi + Mape + Prireži vse naslovnice albumov v razmerje 1:1 + Prilagojeno dejanje na vrstici za predvajanje + Indigo modra + -%.1f dB + Nalaganje glasbe + In (&) + Število pesmi + Sortiraj + Vijolična + Brezplačni format brez izgub zvoka (FLAC) + Neznan žanr + +%.1f dB + Prilagodi + Urejanje %s + Preskoči na naslednjo + Način ponavljanja + + %d album + %d albuma + %d albumi + %d albumov + + Disk + Nalaganje vaše glasbene knjižnice… (%1$d/%2$d) + Počisti iskalno poizvedbo + Naraščajoče + Roza + Vse pesmi + O aplikaciji + Disk %d + Omogočite zaobljene robove na dodatnih elementih uporabniškega vmesnika (zahteva zaobljene naslovnice albumov) + Naslovnica albuma za %s + V živo + Spremenite vidnost in vrstni red zavihkov knjižnice + Počisti shranjeno stanje predvajanja (če obstaja) + Sortiraj po + Ogled + Ustavi predvajanje + Mežanica + Glasba se bo nalagala samo iz map, ki jih dodate. + Način + Remiks singla + Auxio potrebuje dovoljenje za branje vaše glasbene knjižnice + Tema + Knjižnica + Statistika knjižnice + Izenačevalnik + Premakni ta zavihek + Pesem + Slika žanra za %s + Nastavitev virov za nalaganje glasbe + DJ Miksi + Odstrani seznam predvajanja\? + Modra + Temno modra + Zaobljen način + Naloženih izvajalcev: %d + Zvok + Glasba se ne bo nalagala iz the map, ki jih dodate. + Rdeča + Dinamično + Temno + Vklopite ali izklopite naključno predvajanje + Žanr + Predvajanje + Vredu + Ustvari nov seznam predvajanja + Singl + Seznam predvajanja odstranjen + Dovoli + Predvajaj iz vseh pesmi + Obnovi stanje predvajanja + Prilagoditev z oznakami + Predvajanje ob priključitvi slušalk + Vejica (,) + Auxio ikona + Skladba %d + Filtriraj + Prezri avdio datoteke, ki niso glasba, na primer podkaste + Glasbena podlaga + Prilagojeno dejanje v obvestilu + Ponastavi nastavitve + Prijavi napako + Izključi ne-glasbo + Predvajaj naslednje + Izvajalci + Skladba + Naloženih albumov: %d + Nov seznam predvajanja + Informacije napake + Premešaj + Napredno avdio kodiranje (AAC) + Prisilite uporabo kvadratnih naslovnic albumov + Zbirka pesmi v živo + Naslovnice albumov + Rumena + Zelena + Različica + Ogled lastnosti + Velikost + Padajoče + Podaljšan remiks + Podpičje (;) + Hitro + Seznam predvajanja preimenovan + Predvajaj ali začasno ustavi + Rjava + ReplayGain strategija + Izvorna koda + Predvajaj iz izvajalca + Ni map + Prilagoditev kontrol uporabniškega vmesnika in obnašanja + Hitrost vzorčenja + Čakalna vrsta + Ločila za več vrednosti + ReplayGain Tehnologija + Singli + Pregled in nadzor predvajanja glasbe + Uporabite čisto črno temo + Ponovno preglej glasbeno knjižnico + Licence + Format + Vsebina + Preimenuj seznam predvajanja + Bitna hitrost + Odstraniti %s\? Tega ni mogoče razveljaviti. + Nastavitve + Konfigurirajte znake, ki označujejo več vrednosti zaporedoma + Ni skladbe + Opozorilo: Uporaba te nastavitve lahko povzroči, da se nekatere oznake napačno interpretirajo kot oznake z več vrednostmi. To lahko rešite tako, da neželene ločevalne znake predhodno označite z vzvratno poševnico (\\). + Temno zelena + Predvajaj iz žanra + Trajanje + Limeta + Sodeloval pri + %d Hz + \ No newline at end of file diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 60f9e5c97..16af4e8f1 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -3,7 +3,7 @@ Försök igen Musik laddar Laddar musik - Alla låtar + Alla spår Album Albumet Remix-album @@ -23,7 +23,7 @@ Remixar Framträder på Konstnär - Konstnär + Konstnärer Genrer Spellista Spellistor @@ -68,11 +68,11 @@ Licenser Visa och kontrollera musikuppspelning Laddar ditt musikbibliotek… - Övervakning ditt musikbibliotek för ändringar… - Tillagd till kö + Overvåker ditt musikbibliotek för ändringar… + Tillagd i kö Spellista skapade Tillagd till spellista - Sök ditt musikbibliotek… + Sök i ditt musikbibliotek… Inställningar Utseende Ändra tema och färger på appen @@ -83,7 +83,7 @@ Bevilja En enkel, rationell musikspelare för Android. Övervakar musikbiblioteket - Låtar + Spår Live-album Ta bort Live-sammanställning @@ -107,7 +107,7 @@ Tillstånd sparat Version Statistik över beroende - Bytt namn av spellista + Byt namn av spellista Spellista tog bort Utvecklad av Alexander Capeheart Tema @@ -125,7 +125,7 @@ När spelar från artikeluppgifter Spela från genre Komma ihåg blandningsstatus - Behåll blandning på när spelar en ny låt + Behåll blandning på när en ny låt spelas Kontent Kontrollera hur musik och bilar laddas Musik @@ -151,4 +151,146 @@ Konfigurera tecken som separerar flera värden i taggar Advarsel: Denna inställning kan leda till att vissa taggar separeras felaktigt. För att åtgärda detta, prefixa oönskade separatortecken med ett backslash (\\). Anpassa UI-kontroller och beteende + Av + Hörlurar-autouppspelning + Pausa när en låt upprepas + Musik laddas inte från mapparna som ni lägger till. + Öppna kö + Dynamisk + %d konstnärer som laddats + + %d konstnär + %d konstnärer + + Bildar + Ljud + Konfigurera ljud- och uppspelningsbeteende + Spola tillbaka innan spår hoppar tillbaka + ReplayGain-strategi + Rensa det tidigare sparade uppspelningsläget om det finns + Återställ uppspelningsläge + -%.1f dB + Radera %s\? Detta kan inte ångras. + Endast visa artister som är direkt krediterade på ett album (funkar bäst på välmärkta bibliotek) + Albumomslag + Snabbt + Bibliotek + Inkludera + Uppdatera musik + Ladda musikbiblioteket om och använd cachad taggar när det är möjligt + Uthållighet + Rensa uppspelningsläge + Återställ det tidigare lagrade uppspelningsläget om det finns + Misslyckades att spara uppspelningsläget + Blanda alla spår + Rensa sökfrågan + Radera mappen + Genrebild för %s + Spellistabild för %s + MPEG-1-ljud + MPEG-4-ljud + OGG-ljud + Matroska-ljud + Blå + Mörkblå + Cyanblå + Blågrön + Grön + Mörkgrön + Limegrön + Gul + Grå + %1$s, %2$s + Redigerar %s + Uppspelning + Orange + Brun + Alltid börja uppspelning när hörlurar kopplas till (kanske inte fungerar på alla enheter) + Pausa vid upprepa + ReplayGain förförstärkare + Justering utan taggar + Musikmappar + Varning: Om man ändrar förförstärkaren till ett högt positivt värde kan det leda till toppning på vissa ljudspår. + Hantera var musik bör laddas in från + Mappar + Modus + Utesluta + Musik laddas endast från mapparna som ni lägger till. + Spara det aktuella uppspelningsläget + Skanna musik om + Rensa tagbiblioteket och ladda komplett om musikbiblioteket (långsammare, men mer komplett) + Ingen musik på gång + Laddning av musik misslyckades + Auxio behöver tillstånd för att läsa ditt musikbibliotek + Ingen app på gång som kan hantera denna uppgift + Denna mapp stöds inte + Misslyckades att återställa uppspelningsläget + Spår %d + Spela eller pausa + Flytta detta spår + Okänd konstnär + Okänd genre + Avancerad audio-koding (AAC) + %d utvalda + Spellista %d + +%.1f dB + + %d spår + %d spår + + + %d album + %d album + + %d spår som laddats + Total längd: %s + Kopierade + Urval + Felinformation + Rapportera + Ingen datum + Ingen disk + Inget spår + Inga spår + Lilla + %d kbps + %d Hz + %d album som laddats + %d genrer som laddats + Spela upp låten själv + Hög kvalitet + Tvinga fyrkantiga skivomslag + Beskär alla albumomslag till en 1:1 sidförhållande + Spola tillbaka innan att hoppa till föregående låt + Justering med taggar + Inga mappar + Misslyckades att rensa uppspelningsläget + Skapa en ny spellista + Stoppa uppspelning + Radera detta spår + Auxio-ikon + Flytta denna flik + Albumomslag + Urvalbild + Mörklila + Indigo + Disk %d + Spara uppspelningsläge + Hoppa till nästa spår + Hoppa till sista spår + Ändra upprepningsläge + Slå på eller av blandningen + Albumomslag för %s + Konstnärbild för %s + Ingen musik spelas + Fritt tapsfritt ljudkodek (FLAC) + Rosa + Laddar ditt musikbibliotek… (%1$d/%2$d) + Ampersand (&) + ReplayGain + Föredra spår + Föredra album + Föredra album om ett album spelar + Förförstarkning användas för befintliga justeringar vid uppspelning + Röd \ No newline at end of file diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 559ef2336..5e77b99ab 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -197,7 +197,7 @@ Tekli Karışık kaset Canlı derleme - Remiks derlemeler + Remiks derlemesi Ekolayzır Canlı EP Remiks EP @@ -228,8 +228,8 @@ %d sanatçı %d sanatçılar - Karmalar - Karma + DJ Miksleri + DJ Mix Etiket önbelleğini temizleyin ve müzik kitaplığını tamamen yeniden yükleyin (daha yavaş, ancak daha eksiksiz) Çok değerli ayırıcılar Birden fazla etiket değerini ifade eden karakterleri yapılandırın @@ -278,4 +278,30 @@ Yeniden Adlandır Oynatma Listesini Yeniden Adlandır Oynatma listesini silmek istiyor musun\? + Yön + Seçim görüntüsü + Seçim + Şarkıyı kendi kendine çal + Çalma listesi %d + Oynatma listesi oluşturuldu + Daha fazla + Disk yok + Kopyalandı + Çalma listesine ekle + Paylaş + Düzenle + Çalma listesine eklendi + Şarkı yok + Tüm albüm kapaklarını 1:1 en boy oranına kırp + %s düzenleniyor + Göre sırala + Görünüm + Şarkı + Çalma listesi silindi + Rapor + Hata bilgisi + Kare albüm kapaklarına zorla + Çalma listesi yeniden adlandırıldı + %s silinsin mi\? Geri alınamaz. + Üzerinde görünür \ 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 ddeb997f0..f4b12a5aa 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -306,4 +306,8 @@ Напрямок Вибрати Вибрати зображення + Докладніше + Інформація про помилку + Скопійовано + Звіт \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index c5e17d0c4..51381f724 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -300,4 +300,8 @@ 说明 选择 选择图片 + 报告 + 更多 + 已复制 + 错误信息 \ No newline at end of file diff --git a/app/src/test/java/org/oxycblt/auxio/music/FakeMusic.kt b/app/src/test/java/org/oxycblt/auxio/music/FakeMusic.kt deleted file mode 100644 index 32d8c0df2..000000000 --- a/app/src/test/java/org/oxycblt/auxio/music/FakeMusic.kt +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright (c) 2023 Auxio Project - * FakeMusic.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.music - -import android.net.Uri -import org.oxycblt.auxio.music.fs.MimeType -import org.oxycblt.auxio.music.fs.Path -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 - -open class FakeSong : Song { - override val name: Name - get() = throw NotImplementedError() - - override val date: Date? - get() = throw NotImplementedError() - - override val dateAdded: Long - get() = throw NotImplementedError() - - override val disc: Disc? - get() = throw NotImplementedError() - - override val genres: List - get() = throw NotImplementedError() - - override val mimeType: MimeType - get() = throw NotImplementedError() - - override val track: Int? - get() = throw NotImplementedError() - - override val path: Path - get() = throw NotImplementedError() - - override val size: Long - get() = throw NotImplementedError() - - override val uri: Uri - get() = throw NotImplementedError() - - override val album: Album - get() = throw NotImplementedError() - - override val artists: List - get() = throw NotImplementedError() - - override val durationMs: Long - get() = throw NotImplementedError() - - override val uid: Music.UID - get() = throw NotImplementedError() -} - -open class FakeAlbum : Album { - override val name: Name - get() = throw NotImplementedError() - - override val coverUri: Uri - get() = throw NotImplementedError() - - override val dateAdded: Long - get() = throw NotImplementedError() - - override val dates: Date.Range? - get() = throw NotImplementedError() - - override val releaseType: ReleaseType - get() = throw NotImplementedError() - - override val artists: List - get() = throw NotImplementedError() - - override val durationMs: Long - get() = throw NotImplementedError() - - override val songs: List - get() = throw NotImplementedError() - - override val uid: Music.UID - get() = throw NotImplementedError() -} - -open class FakeArtist : Artist { - override val name: Name - get() = throw NotImplementedError() - - override val albums: List - get() = throw NotImplementedError() - - override val explicitAlbums: List - get() = throw NotImplementedError() - - override val implicitAlbums: List - get() = throw NotImplementedError() - - override val genres: List - get() = throw NotImplementedError() - - override val durationMs: Long - get() = throw NotImplementedError() - - override val songs: List - get() = throw NotImplementedError() - - override val uid: Music.UID - get() = throw NotImplementedError() -} - -open class FakeGenre : Genre { - override val name: Name - get() = throw NotImplementedError() - - override val artists: List - get() = throw NotImplementedError() - - override val durationMs: Long - get() = throw NotImplementedError() - - override val songs: List - get() = throw NotImplementedError() - - override val uid: Music.UID - get() = throw NotImplementedError() -} diff --git a/app/src/test/java/org/oxycblt/auxio/music/FakeMusicRepository.kt b/app/src/test/java/org/oxycblt/auxio/music/FakeMusicRepository.kt deleted file mode 100644 index 8c79f0e9a..000000000 --- a/app/src/test/java/org/oxycblt/auxio/music/FakeMusicRepository.kt +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (c) 2023 Auxio Project - * FakeMusicRepository.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.music - -import kotlinx.coroutines.Job -import org.oxycblt.auxio.music.device.DeviceLibrary -import org.oxycblt.auxio.music.user.UserLibrary - -open class FakeMusicRepository : MusicRepository { - override val indexingState: IndexingState? - get() = throw NotImplementedError() - - override val deviceLibrary: DeviceLibrary? - get() = throw NotImplementedError() - - override val userLibrary: UserLibrary? - get() = throw NotImplementedError() - - override fun addUpdateListener(listener: MusicRepository.UpdateListener) { - throw NotImplementedError() - } - - override fun removeUpdateListener(listener: MusicRepository.UpdateListener) { - throw NotImplementedError() - } - - override fun addIndexingListener(listener: MusicRepository.IndexingListener) { - throw NotImplementedError() - } - - override fun removeIndexingListener(listener: MusicRepository.IndexingListener) { - throw NotImplementedError() - } - - override fun registerWorker(worker: MusicRepository.IndexingWorker) { - throw NotImplementedError() - } - - override fun unregisterWorker(worker: MusicRepository.IndexingWorker) { - throw NotImplementedError() - } - - override fun find(uid: Music.UID): Music? { - throw NotImplementedError() - } - - override suspend fun createPlaylist(name: String, songs: List) { - throw NotImplementedError() - } - - override suspend fun renamePlaylist(playlist: Playlist, name: String) { - throw NotImplementedError() - } - - override suspend fun deletePlaylist(playlist: Playlist) { - throw NotImplementedError() - } - - override suspend fun addToPlaylist(songs: List, playlist: Playlist) { - throw NotImplementedError() - } - - override suspend fun rewritePlaylist(playlist: Playlist, songs: List) { - throw NotImplementedError() - } - - override fun requestIndex(withCache: Boolean) { - throw NotImplementedError() - } - - override fun index(worker: MusicRepository.IndexingWorker, withCache: Boolean): Job { - throw NotImplementedError() - } -} diff --git a/app/src/test/java/org/oxycblt/auxio/music/FakeMusicSettings.kt b/app/src/test/java/org/oxycblt/auxio/music/FakeMusicSettings.kt deleted file mode 100644 index 14924f4f1..000000000 --- a/app/src/test/java/org/oxycblt/auxio/music/FakeMusicSettings.kt +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (c) 2023 Auxio Project - * FakeMusicSettings.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.music - -import org.oxycblt.auxio.list.sort.Sort -import org.oxycblt.auxio.music.fs.MusicDirectories - -open class FakeMusicSettings : MusicSettings { - override fun registerListener(listener: MusicSettings.Listener) = throw NotImplementedError() - - override fun unregisterListener(listener: MusicSettings.Listener) = throw NotImplementedError() - - override var musicDirs: MusicDirectories - get() = throw NotImplementedError() - set(_) = throw NotImplementedError() - - override val excludeNonMusic: Boolean - get() = throw NotImplementedError() - - override val shouldBeObserving: Boolean - get() = throw NotImplementedError() - - override var multiValueSeparators: String - get() = throw NotImplementedError() - set(_) = throw NotImplementedError() - - override val intelligentSorting: Boolean - get() = throw NotImplementedError() - - override var songSort: Sort - get() = throw NotImplementedError() - set(_) = throw NotImplementedError() - - override var albumSort: Sort - get() = throw NotImplementedError() - set(_) = throw NotImplementedError() - - override var artistSort: Sort - get() = throw NotImplementedError() - set(_) = throw NotImplementedError() - - override var genreSort: Sort - get() = throw NotImplementedError() - set(_) = throw NotImplementedError() - - override var playlistSort: Sort - get() = throw NotImplementedError() - set(_) = throw NotImplementedError() - - override var albumSongSort: Sort - get() = throw NotImplementedError() - set(_) = throw NotImplementedError() - - override var artistSongSort: Sort - get() = throw NotImplementedError() - set(_) = throw NotImplementedError() - - override var genreSongSort: Sort - get() = throw NotImplementedError() - set(_) = throw NotImplementedError() -} diff --git a/app/src/test/java/org/oxycblt/auxio/music/MusicModeTest.kt b/app/src/test/java/org/oxycblt/auxio/music/MusicModeTest.kt deleted file mode 100644 index c11985970..000000000 --- a/app/src/test/java/org/oxycblt/auxio/music/MusicModeTest.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (c) 2023 Auxio Project - * MusicModeTest.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.music - -import org.junit.Assert.assertEquals -import org.junit.Test - -class MusicModeTest { - @Test - fun intCode() { - assertEquals(MusicType.SONGS, MusicType.fromIntCode(MusicType.SONGS.intCode)) - assertEquals(MusicType.ALBUMS, MusicType.fromIntCode(MusicType.ALBUMS.intCode)) - assertEquals(MusicType.ARTISTS, MusicType.fromIntCode(MusicType.ARTISTS.intCode)) - assertEquals(MusicType.GENRES, MusicType.fromIntCode(MusicType.GENRES.intCode)) - } -} diff --git a/app/src/test/java/org/oxycblt/auxio/music/MusicViewModelTest.kt b/app/src/test/java/org/oxycblt/auxio/music/MusicViewModelTest.kt deleted file mode 100644 index b25c2c0b1..000000000 --- a/app/src/test/java/org/oxycblt/auxio/music/MusicViewModelTest.kt +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright (c) 2023 Auxio Project - * MusicViewModelTest.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.music - -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue -import org.junit.Test -import org.oxycblt.auxio.music.device.DeviceLibrary -import org.oxycblt.auxio.music.device.FakeDeviceLibrary -import org.oxycblt.auxio.util.forceClear - -class MusicViewModelTest { - @Test - fun indexerState() { - val indexer = - TestMusicRepository().apply { - indexingState = IndexingState.Indexing(IndexingProgress.Indeterminate) - } - val musicViewModel = MusicViewModel(indexer, FakeMusicSettings()) - assertTrue(indexer.updateListener is MusicViewModel) - assertTrue(indexer.indexingListener is MusicViewModel) - assertEquals( - IndexingProgress.Indeterminate, - (musicViewModel.indexingState.value as IndexingState.Indexing).progress) - indexer.indexingState = null - assertEquals(null, musicViewModel.indexingState.value) - musicViewModel.forceClear() - assertTrue(indexer.indexingListener == null) - } - - @Test - fun statistics() { - val musicRepository = TestMusicRepository() - val musicViewModel = MusicViewModel(musicRepository, FakeMusicSettings()) - assertEquals(null, musicViewModel.statistics.value) - musicRepository.deviceLibrary = TestDeviceLibrary() - assertEquals( - MusicViewModel.Statistics( - 2, - 3, - 4, - 1, - 161616 * 2, - ), - musicViewModel.statistics.value) - } - - @Test - fun requests() { - val indexer = TestMusicRepository() - val musicViewModel = MusicViewModel(indexer, FakeMusicSettings()) - musicViewModel.refresh() - musicViewModel.rescan() - assertEquals(listOf(true, false), indexer.requests) - } - - private class TestMusicRepository : FakeMusicRepository() { - override var deviceLibrary: DeviceLibrary? = null - set(value) { - field = value - updateListener?.onMusicChanges( - MusicRepository.Changes(deviceLibrary = true, userLibrary = false)) - } - - override var indexingState: IndexingState? = null - set(value) { - field = value - indexingListener?.onIndexingStateChanged() - } - - var updateListener: MusicRepository.UpdateListener? = null - var indexingListener: MusicRepository.IndexingListener? = null - val requests = mutableListOf() - - override fun addUpdateListener(listener: MusicRepository.UpdateListener) { - listener.onMusicChanges( - MusicRepository.Changes(deviceLibrary = true, userLibrary = false)) - this.updateListener = listener - } - - override fun removeUpdateListener(listener: MusicRepository.UpdateListener) { - this.updateListener = null - } - - override fun addIndexingListener(listener: MusicRepository.IndexingListener) { - listener.onIndexingStateChanged() - this.indexingListener = listener - } - - override fun removeIndexingListener(listener: MusicRepository.IndexingListener) { - this.indexingListener = null - } - - override fun requestIndex(withCache: Boolean) { - requests.add(withCache) - } - } - - private class TestDeviceLibrary : FakeDeviceLibrary() { - override val songs: List - get() = listOf(TestSong(), TestSong()) - - override val albums: List - get() = listOf(FakeAlbum(), FakeAlbum(), FakeAlbum()) - - override val artists: List - get() = listOf(FakeArtist(), FakeArtist(), FakeArtist(), FakeArtist()) - - override val genres: List - get() = listOf(FakeGenre()) - } - - private class TestSong : FakeSong() { - override val durationMs: Long - get() = 161616 - } -} diff --git a/app/src/test/java/org/oxycblt/auxio/music/cache/CacheRepositoryTest.kt b/app/src/test/java/org/oxycblt/auxio/music/cache/CacheRepositoryTest.kt new file mode 100644 index 000000000..9914dbe5f --- /dev/null +++ b/app/src/test/java/org/oxycblt/auxio/music/cache/CacheRepositoryTest.kt @@ -0,0 +1,266 @@ +/* + * Copyright (c) 2023 Auxio Project + * CacheRepositoryTest.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.music.cache + +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.coVerifyAll +import io.mockk.coVerifySequence +import io.mockk.just +import io.mockk.mockk +import io.mockk.slot +import java.lang.IllegalStateException +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.oxycblt.auxio.music.device.RawSong +import org.oxycblt.auxio.music.info.Date + +class CacheRepositoryTest { + @Test + fun cache_read_noInvalidate() { + val dao = + mockk { + coEvery { readSongs() }.returnsMany(listOf(CACHED_SONG_A, CACHED_SONG_B)) + } + val cacheRepository = CacheRepositoryImpl(dao) + val cache = requireNotNull(runBlocking { cacheRepository.readCache() }) + coVerifyAll { dao.readSongs() } + assertFalse(cache.invalidated) + + val songA = RawSong(mediaStoreId = 0, dateAdded = 1, dateModified = 2) + assertTrue(cache.populate(songA)) + assertEquals(RAW_SONG_A, songA) + + assertFalse(cache.invalidated) + + val songB = RawSong(mediaStoreId = 9, dateAdded = 10, dateModified = 11) + assertTrue(cache.populate(songB)) + assertEquals(RAW_SONG_B, songB) + + assertFalse(cache.invalidated) + } + + @Test + fun cache_read_invalidate() { + val dao = + mockk { + coEvery { readSongs() }.returnsMany(listOf(CACHED_SONG_A, CACHED_SONG_B)) + } + val cacheRepository = CacheRepositoryImpl(dao) + val cache = requireNotNull(runBlocking { cacheRepository.readCache() }) + coVerifyAll { dao.readSongs() } + assertFalse(cache.invalidated) + + val nullStart = RawSong(mediaStoreId = 0, dateAdded = 0, dateModified = 0) + val nullEnd = RawSong(mediaStoreId = 0, dateAdded = 0, dateModified = 0) + assertFalse(cache.populate(nullStart)) + assertEquals(nullStart, nullEnd) + + assertTrue(cache.invalidated) + + val songB = RawSong(mediaStoreId = 9, dateAdded = 10, dateModified = 11) + assertTrue(cache.populate(songB)) + assertEquals(RAW_SONG_B, songB) + + assertTrue(cache.invalidated) + } + + @Test + fun cache_read_crashes() { + val dao = mockk { coEvery { readSongs() } throws IllegalStateException() } + val cacheRepository = CacheRepositoryImpl(dao) + assertEquals(null, runBlocking { cacheRepository.readCache() }) + coVerifyAll { dao.readSongs() } + } + + @Test + fun cache_write() { + var currentlyStoredSongs = listOf() + val insertSongsArg = slot>() + val dao = + mockk { + coEvery { nukeSongs() } answers { currentlyStoredSongs = listOf() } + + coEvery { insertSongs(capture(insertSongsArg)) } answers + { + currentlyStoredSongs = insertSongsArg.captured + } + } + + val cacheRepository = CacheRepositoryImpl(dao) + + val rawSongs = listOf(RAW_SONG_A, RAW_SONG_B) + runBlocking { cacheRepository.writeCache(rawSongs) } + + val cachedSongs = listOf(CACHED_SONG_A, CACHED_SONG_B) + coVerifySequence { + dao.nukeSongs() + dao.insertSongs(cachedSongs) + } + assertEquals(cachedSongs, currentlyStoredSongs) + } + + @Test + fun cache_write_nukeCrashes() { + val dao = + mockk { + coEvery { nukeSongs() } throws IllegalStateException() + coEvery { insertSongs(listOf()) } just Runs + } + val cacheRepository = CacheRepositoryImpl(dao) + runBlocking { cacheRepository.writeCache(listOf()) } + coVerifyAll { dao.nukeSongs() } + } + + @Test + fun cache_write_insertCrashes() { + val dao = + mockk { + coEvery { nukeSongs() } just Runs + coEvery { insertSongs(listOf()) } throws IllegalStateException() + } + val cacheRepository = CacheRepositoryImpl(dao) + runBlocking { cacheRepository.writeCache(listOf()) } + coVerifySequence { + dao.nukeSongs() + dao.insertSongs(listOf()) + } + } + + private companion object { + val CACHED_SONG_A = + CachedSong( + mediaStoreId = 0, + dateAdded = 1, + dateModified = 2, + size = 3, + durationMs = 4, + replayGainTrackAdjustment = 5.5f, + replayGainAlbumAdjustment = 6.6f, + musicBrainzId = "Song MBID A", + name = "Song Name A", + sortName = "Song Sort Name A", + track = 7, + disc = 8, + subtitle = "Subtitle A", + date = Date.from("2020-10-10"), + albumMusicBrainzId = "Album MBID A", + albumName = "Album Name A", + albumSortName = "Album Sort Name A", + releaseTypes = listOf("Release Type A"), + artistMusicBrainzIds = listOf("Artist MBID A"), + artistNames = listOf("Artist Name A"), + artistSortNames = listOf("Artist Sort Name A"), + albumArtistMusicBrainzIds = listOf("Album Artist MBID A"), + albumArtistNames = listOf("Album Artist Name A"), + albumArtistSortNames = listOf("Album Artist Sort Name A"), + genreNames = listOf("Genre Name A"), + ) + + val RAW_SONG_A = + RawSong( + mediaStoreId = 0, + dateAdded = 1, + dateModified = 2, + size = 3, + durationMs = 4, + replayGainTrackAdjustment = 5.5f, + replayGainAlbumAdjustment = 6.6f, + musicBrainzId = "Song MBID A", + name = "Song Name A", + sortName = "Song Sort Name A", + track = 7, + disc = 8, + subtitle = "Subtitle A", + date = Date.from("2020-10-10"), + albumMusicBrainzId = "Album MBID A", + albumName = "Album Name A", + albumSortName = "Album Sort Name A", + releaseTypes = listOf("Release Type A"), + artistMusicBrainzIds = listOf("Artist MBID A"), + artistNames = listOf("Artist Name A"), + artistSortNames = listOf("Artist Sort Name A"), + albumArtistMusicBrainzIds = listOf("Album Artist MBID A"), + albumArtistNames = listOf("Album Artist Name A"), + albumArtistSortNames = listOf("Album Artist Sort Name A"), + genreNames = listOf("Genre Name A"), + ) + + val CACHED_SONG_B = + CachedSong( + mediaStoreId = 9, + dateAdded = 10, + dateModified = 11, + size = 12, + durationMs = 13, + replayGainTrackAdjustment = 14.14f, + replayGainAlbumAdjustment = 15.15f, + musicBrainzId = "Song MBID B", + name = "Song Name B", + sortName = "Song Sort Name B", + track = 16, + disc = 17, + subtitle = "Subtitle B", + date = Date.from("2021-11-11"), + albumMusicBrainzId = "Album MBID B", + albumName = "Album Name B", + albumSortName = "Album Sort Name B", + releaseTypes = listOf("Release Type B"), + artistMusicBrainzIds = listOf("Artist MBID B"), + artistNames = listOf("Artist Name B"), + artistSortNames = listOf("Artist Sort Name B"), + albumArtistMusicBrainzIds = listOf("Album Artist MBID B"), + albumArtistNames = listOf("Album Artist Name B"), + albumArtistSortNames = listOf("Album Artist Sort Name B"), + genreNames = listOf("Genre Name B"), + ) + + val RAW_SONG_B = + RawSong( + mediaStoreId = 9, + dateAdded = 10, + dateModified = 11, + size = 12, + durationMs = 13, + replayGainTrackAdjustment = 14.14f, + replayGainAlbumAdjustment = 15.15f, + musicBrainzId = "Song MBID B", + name = "Song Name B", + sortName = "Song Sort Name B", + track = 16, + disc = 17, + subtitle = "Subtitle B", + date = Date.from("2021-11-11"), + albumMusicBrainzId = "Album MBID B", + albumName = "Album Name B", + albumSortName = "Album Sort Name B", + releaseTypes = listOf("Release Type B"), + artistMusicBrainzIds = listOf("Artist MBID B"), + artistNames = listOf("Artist Name B"), + artistSortNames = listOf("Artist Sort Name B"), + albumArtistMusicBrainzIds = listOf("Album Artist MBID B"), + albumArtistNames = listOf("Album Artist Name B"), + albumArtistSortNames = listOf("Album Artist Sort Name B"), + genreNames = listOf("Genre Name B"), + ) + } +} diff --git a/app/src/test/java/org/oxycblt/auxio/music/device/DeviceMusicImplTest.kt b/app/src/test/java/org/oxycblt/auxio/music/device/DeviceMusicImplTest.kt deleted file mode 100644 index 2c4805486..000000000 --- a/app/src/test/java/org/oxycblt/auxio/music/device/DeviceMusicImplTest.kt +++ /dev/null @@ -1,186 +0,0 @@ -/* - * Copyright (c) 2023 Auxio Project - * DeviceMusicImplTest.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.music.device - -import java.util.UUID -import org.junit.Assert.assertTrue -import org.junit.Test - -class DeviceMusicImplTest { - @Test - fun albumRaw_equals_inconsistentCase() { - val a = - RawAlbum( - mediaStoreId = -1, - musicBrainzId = null, - name = "Paraglow", - sortName = null, - releaseType = null, - rawArtists = listOf(RawArtist(name = "Parannoul"), RawArtist(name = "Asian Glow"))) - val b = - RawAlbum( - mediaStoreId = -1, - musicBrainzId = null, - name = "paraglow", - sortName = null, - releaseType = null, - rawArtists = listOf(RawArtist(name = "Parannoul"), RawArtist(name = "Asian glow"))) - assertTrue(a == b) - assertTrue(a.hashCode() == b.hashCode()) - } - - @Test - fun albumRaw_equals_withMbids() { - val a = - RawAlbum( - mediaStoreId = -1, - musicBrainzId = UUID.fromString("c7b245c9-8099-32ea-af95-893acedde2cf"), - name = "Weezer", - sortName = "Blue Album", - releaseType = null, - rawArtists = listOf(RawArtist(name = "Weezer"))) - val b = - RawAlbum( - mediaStoreId = -1, - musicBrainzId = UUID.fromString("923d5ba6-7eee-3bce-bcb2-c913b2bd69d4"), - name = "Weezer", - sortName = "Green Album", - releaseType = null, - rawArtists = listOf(RawArtist(name = "Weezer"))) - assertTrue(a != b) - assertTrue(a.hashCode() != b.hashCode()) - } - - @Test - fun albumRaw_equals_inconsistentMbids() { - val a = - RawAlbum( - mediaStoreId = -1, - musicBrainzId = UUID.fromString("c7b245c9-8099-32ea-af95-893acedde2cf"), - name = "Weezer", - sortName = "Blue Album", - releaseType = null, - rawArtists = listOf(RawArtist(name = "Weezer"))) - val b = - RawAlbum( - mediaStoreId = -1, - musicBrainzId = null, - name = "Weezer", - sortName = "Green Album", - releaseType = null, - rawArtists = listOf(RawArtist(name = "Weezer"))) - assertTrue(a != b) - assertTrue(a.hashCode() != b.hashCode()) - } - - @Test - fun albumRaw_equals_withArtists() { - val a = - RawAlbum( - mediaStoreId = -1, - musicBrainzId = null, - name = "Album", - sortName = null, - releaseType = null, - rawArtists = listOf(RawArtist(name = "Artist A"))) - val b = - RawAlbum( - mediaStoreId = -1, - musicBrainzId = null, - name = "Album", - sortName = null, - releaseType = null, - rawArtists = listOf(RawArtist(name = "Artist B"))) - assertTrue(a != b) - assertTrue(a.hashCode() != b.hashCode()) - } - - @Test - fun artistRaw_equals_inconsistentCase() { - val a = RawArtist(musicBrainzId = null, name = "Parannoul") - val b = RawArtist(musicBrainzId = null, name = "parannoul") - assertTrue(a == b) - assertTrue(a.hashCode() == b.hashCode()) - } - - @Test - fun artistRaw_equals_withMbids() { - val a = - RawArtist( - musicBrainzId = UUID.fromString("677325ef-d850-44bb-8258-0d69bbc0b3f7"), - name = "Artist") - val b = - RawArtist( - musicBrainzId = UUID.fromString("6b625592-d88d-48c8-ac1a-c5b476d78bcc"), - name = "Artist") - assertTrue(a != b) - assertTrue(a.hashCode() != b.hashCode()) - } - - @Test - fun artistRaw_equals_inconsistentMbids() { - val a = - RawArtist( - musicBrainzId = UUID.fromString("677325ef-d850-44bb-8258-0d69bbc0b3f7"), - name = "Artist") - val b = RawArtist(musicBrainzId = null, name = "Artist") - assertTrue(a != b) - assertTrue(a.hashCode() != b.hashCode()) - } - - @Test - fun artistRaw_equals_missingNames() { - val a = RawArtist(name = null) - val b = RawArtist(name = null) - assertTrue(a == b) - assertTrue(a.hashCode() == b.hashCode()) - } - - @Test - fun artistRaw_equals_inconsistentNames() { - val a = RawArtist(name = null) - val b = RawArtist(name = "Parannoul") - assertTrue(a != b) - assertTrue(a.hashCode() != b.hashCode()) - } - - @Test - fun genreRaw_equals_inconsistentCase() { - val a = RawGenre("Future Garage") - val b = RawGenre("future garage") - assertTrue(a == b) - assertTrue(a.hashCode() == b.hashCode()) - } - - @Test - fun genreRaw_equals_missingNames() { - val a = RawGenre(name = null) - val b = RawGenre(name = null) - assertTrue(a == b) - assertTrue(a.hashCode() == b.hashCode()) - } - - @Test - fun genreRaw_equals_inconsistentNames() { - val a = RawGenre(name = null) - val b = RawGenre(name = "Future Garage") - assertTrue(a != b) - assertTrue(a.hashCode() != b.hashCode()) - } -} diff --git a/app/src/test/java/org/oxycblt/auxio/music/device/FakeDeviceLibrary.kt b/app/src/test/java/org/oxycblt/auxio/music/device/FakeDeviceLibrary.kt deleted file mode 100644 index dab0834a3..000000000 --- a/app/src/test/java/org/oxycblt/auxio/music/device/FakeDeviceLibrary.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2023 Auxio Project - * FakeDeviceLibrary.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.music.device - -import android.content.Context -import android.net.Uri -import org.oxycblt.auxio.music.Album -import org.oxycblt.auxio.music.Artist -import org.oxycblt.auxio.music.Genre -import org.oxycblt.auxio.music.Music -import org.oxycblt.auxio.music.Song - -open class FakeDeviceLibrary : DeviceLibrary { - override val songs: List - get() = throw NotImplementedError() - - override val albums: List - get() = throw NotImplementedError() - - override val artists: List - get() = throw NotImplementedError() - - override val genres: List - get() = throw NotImplementedError() - - override fun findSong(uid: Music.UID): Song? { - throw NotImplementedError() - } - - override fun findSongForUri(context: Context, uri: Uri): Song? { - throw NotImplementedError() - } - - override fun findAlbum(uid: Music.UID): Album? { - throw NotImplementedError() - } - - override fun findArtist(uid: Music.UID): Artist? { - throw NotImplementedError() - } - - override fun findGenre(uid: Music.UID): Genre? { - throw NotImplementedError() - } -} diff --git a/app/src/test/java/org/oxycblt/auxio/music/info/DateTest.kt b/app/src/test/java/org/oxycblt/auxio/music/info/DateTest.kt index 075df1b1c..b63639e27 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/info/DateTest.kt +++ b/app/src/test/java/org/oxycblt/auxio/music/info/DateTest.kt @@ -88,32 +88,4 @@ class DateTest { assertEquals(null, Date.from("2016-08-16:00:01:02")) assertEquals("2016-11", Date.from("2016-11-32 25:43:01").toString()) } - - @Test - fun dateRange_from_correct() { - val range = - requireNotNull( - Date.Range.from( - listOf( - requireNotNull(Date.from("2016-08-16T00:01:02")), - requireNotNull(Date.from("2016-07-16")), - requireNotNull(Date.from("2014-03-12T00")), - requireNotNull(Date.from("2022-12-22T22:22:22"))))) - assertEquals("2014-03-12T00Z", range.min.toString()) - assertEquals("2022-12-22T22:22:22Z", range.max.toString()) - } - - @Test - fun dateRange_from_one() { - val range = - requireNotNull( - Date.Range.from(listOf(requireNotNull(Date.from("2016-08-16T00:01:02"))))) - assertEquals("2016-08-16T00:01:02Z", range.min.toString()) - assertEquals("2016-08-16T00:01:02Z", range.max.toString()) - } - - @Test - fun dateRange_from_none() { - assertEquals(null, Date.Range.from(listOf())) - } } diff --git a/app/src/test/java/org/oxycblt/auxio/music/info/DiscTest.kt b/app/src/test/java/org/oxycblt/auxio/music/info/DiscTest.kt index 260ca67cb..9b428acac 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/info/DiscTest.kt +++ b/app/src/test/java/org/oxycblt/auxio/music/info/DiscTest.kt @@ -19,30 +19,36 @@ package org.oxycblt.auxio.music.info import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue import org.junit.Test class DiscTest { @Test - fun disc_compare() { - val a = Disc(1, "Part I") - val b = Disc(2, "Part II") - assertEquals(-1, a.compareTo(b)) + fun disc_equals_byNum() { + val a = Disc(0, null) + val b = Disc(0, null) + assertEquals(a, b) + assertEquals(a.hashCode(), b.hashCode()) } @Test - fun disc_equals_correct() { - val a = Disc(1, "Part I") - val b = Disc(1, "Part I") - assertTrue(a == b) - assertTrue(a.hashCode() == b.hashCode()) + fun disc_equals_bySubtitle() { + val a = Disc(0, "z subtitle") + val b = Disc(0, "a subtitle") + assertEquals(a, b) + assertEquals(a.hashCode(), b.hashCode()) } @Test - fun disc_equals_inconsistentNames() { - val a = Disc(1, "Part I") + fun disc_compareTo_byNum() { + val a = Disc(0, null) val b = Disc(1, null) - assertTrue(a == b) - assertTrue(a.hashCode() == b.hashCode()) + assertEquals(-1, a.compareTo(b)) + } + + @Test + fun disc_compareTo_bySubtitle() { + val a = Disc(0, "z subtitle") + val b = Disc(1, "a subtitle") + assertEquals(-1, a.compareTo(b)) } } diff --git a/app/src/test/java/org/oxycblt/auxio/music/info/NameTest.kt b/app/src/test/java/org/oxycblt/auxio/music/info/NameTest.kt new file mode 100644 index 000000000..fd80d51c4 --- /dev/null +++ b/app/src/test/java/org/oxycblt/auxio/music/info/NameTest.kt @@ -0,0 +1,453 @@ +/* + * Copyright (c) 2023 Auxio Project + * NameTest.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.music.info + +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.oxycblt.auxio.music.MusicSettings + +class NameTest { + @Test + fun name_simple_from_settings() { + val musicSettings = mockk { every { intelligentSorting } returns false } + assertTrue(Name.Known.Factory.from(musicSettings) is SimpleKnownName.Factory) + } + + @Test + fun name_intelligent_from_settings() { + val musicSettings = mockk { every { intelligentSorting } returns true } + assertTrue(Name.Known.Factory.from(musicSettings) is IntelligentKnownName.Factory) + } + + @Test + fun name_simple_withoutPunct() { + val name = SimpleKnownName("Loveless", null) + assertEquals("Loveless", name.raw) + assertEquals(null, name.sort) + assertEquals("L", name.thumb) + val only = name.sortTokens.single() + assertEquals("Loveless", only.collationKey.sourceString) + assertEquals(SortToken.Type.LEXICOGRAPHIC, only.type) + } + + @Test + fun name_simple_withPunct() { + val name = SimpleKnownName("alt-J", null) + assertEquals("alt-J", name.raw) + assertEquals(null, name.sort) + assertEquals("A", name.thumb) + val only = name.sortTokens.single() + assertEquals("altJ", only.collationKey.sourceString) + assertEquals(SortToken.Type.LEXICOGRAPHIC, only.type) + } + + @Test + fun name_simple_oopsAllPunct() { + val name = SimpleKnownName("!!!", null) + assertEquals("!!!", name.raw) + assertEquals(null, name.sort) + assertEquals("!", name.thumb) + val only = name.sortTokens.single() + assertEquals("!!!", only.collationKey.sourceString) + assertEquals(SortToken.Type.LEXICOGRAPHIC, only.type) + } + + @Test + fun name_simple_spacedPunct() { + val name = SimpleKnownName("& Yet & Yet", null) + assertEquals("& Yet & Yet", name.raw) + assertEquals(null, name.sort) + assertEquals("Y", name.thumb) + val first = name.sortTokens[0] + assertEquals("Yet Yet", first.collationKey.sourceString) + assertEquals(SortToken.Type.LEXICOGRAPHIC, first.type) + } + + @Test + fun name_simple_withSort() { + val name = SimpleKnownName("The Smile", "Smile") + assertEquals("The Smile", name.raw) + assertEquals("Smile", name.sort) + assertEquals("S", name.thumb) + val only = name.sortTokens.single() + assertEquals("Smile", only.collationKey.sourceString) + assertEquals(SortToken.Type.LEXICOGRAPHIC, only.type) + } + + @Test + fun name_intelligent_withoutPunct_withoutArticle_withoutNumerics() { + val name = IntelligentKnownName("Loveless", null) + assertEquals("Loveless", name.raw) + assertEquals(null, name.sort) + assertEquals("L", name.thumb) + val only = name.sortTokens.single() + assertEquals("Loveless", only.collationKey.sourceString) + assertEquals(SortToken.Type.LEXICOGRAPHIC, only.type) + } + + @Test + fun name_intelligent_withoutPunct_withoutArticle_withSpacedStartNumerics() { + val name = IntelligentKnownName("15 Step", null) + assertEquals("15 Step", name.raw) + assertEquals(null, name.sort) + assertEquals("#", name.thumb) + val first = name.sortTokens[0] + assertEquals("15", first.collationKey.sourceString) + assertEquals(SortToken.Type.NUMERIC, first.type) + val second = name.sortTokens[1] + assertEquals("Step", second.collationKey.sourceString) + assertEquals(SortToken.Type.LEXICOGRAPHIC, second.type) + } + + @Test + fun name_intelligent_withoutPunct_withoutArticle_withPackedStartNumerics() { + val name = IntelligentKnownName("23Kid", null) + assertEquals("23Kid", name.raw) + assertEquals(null, name.sort) + assertEquals("#", name.thumb) + val first = name.sortTokens[0] + assertEquals("23", first.collationKey.sourceString) + assertEquals(SortToken.Type.NUMERIC, first.type) + val second = name.sortTokens[1] + assertEquals("Kid", second.collationKey.sourceString) + assertEquals(SortToken.Type.LEXICOGRAPHIC, second.type) + } + + @Test + fun name_intelligent_withoutPunct_withoutArticle_withSpacedMiddleNumerics() { + val name = IntelligentKnownName("Foo 1 2 Bar", null) + assertEquals("Foo 1 2 Bar", name.raw) + assertEquals(null, name.sort) + assertEquals("F", name.thumb) + val first = name.sortTokens[0] + assertEquals("Foo", first.collationKey.sourceString) + assertEquals(SortToken.Type.LEXICOGRAPHIC, first.type) + val second = name.sortTokens[1] + assertEquals("1", second.collationKey.sourceString) + assertEquals(SortToken.Type.NUMERIC, second.type) + val third = name.sortTokens[2] + assertEquals(" ", third.collationKey.sourceString) + assertEquals(SortToken.Type.LEXICOGRAPHIC, third.type) + val fourth = name.sortTokens[3] + assertEquals("2", fourth.collationKey.sourceString) + assertEquals(SortToken.Type.NUMERIC, fourth.type) + val fifth = name.sortTokens[4] + assertEquals("Bar", fifth.collationKey.sourceString) + assertEquals(SortToken.Type.LEXICOGRAPHIC, fifth.type) + } + + @Test + fun name_intelligent_withoutPunct_withoutArticle_withPackedMiddleNumerics() { + val name = IntelligentKnownName("Foo12Bar", null) + assertEquals("Foo12Bar", name.raw) + assertEquals(null, name.sort) + assertEquals("F", name.thumb) + val first = name.sortTokens[0] + assertEquals("Foo", first.collationKey.sourceString) + assertEquals(SortToken.Type.LEXICOGRAPHIC, first.type) + val second = name.sortTokens[1] + assertEquals("12", second.collationKey.sourceString) + assertEquals(SortToken.Type.NUMERIC, second.type) + val third = name.sortTokens[2] + assertEquals("Bar", third.collationKey.sourceString) + assertEquals(SortToken.Type.LEXICOGRAPHIC, third.type) + } + + @Test + fun name_intelligent_withoutPunct_withoutArticle_withSpacedEndNumerics() { + val name = IntelligentKnownName("Foo 1", null) + assertEquals("Foo 1", name.raw) + assertEquals(null, name.sort) + assertEquals("F", name.thumb) + val first = name.sortTokens[0] + assertEquals("Foo", first.collationKey.sourceString) + assertEquals(SortToken.Type.LEXICOGRAPHIC, first.type) + val second = name.sortTokens[1] + assertEquals("1", second.collationKey.sourceString) + assertEquals(SortToken.Type.NUMERIC, second.type) + } + + @Test + fun name_intelligent_withoutPunct_withoutArticle_withPackedEndNumerics() { + val name = IntelligentKnownName("Error404", null) + assertEquals("Error404", name.raw) + assertEquals(null, name.sort) + assertEquals("E", name.thumb) + val first = name.sortTokens[0] + assertEquals("Error", first.collationKey.sourceString) + assertEquals(SortToken.Type.LEXICOGRAPHIC, first.type) + val second = name.sortTokens[1] + assertEquals("404", second.collationKey.sourceString) + assertEquals(SortToken.Type.NUMERIC, second.type) + } + + @Test + fun name_intelligent_withoutPunct_withThe_withoutNumerics() { + val name = IntelligentKnownName("The National Anthem", null) + assertEquals("The National Anthem", name.raw) + assertEquals(null, name.sort) + assertEquals("N", name.thumb) + val first = name.sortTokens[0] + assertEquals("National Anthem", first.collationKey.sourceString) + assertEquals(SortToken.Type.LEXICOGRAPHIC, first.type) + } + + @Test + fun name_intelligent_withoutPunct_withAn_withoutNumerics() { + val name = IntelligentKnownName("An Eagle in Your Mind", null) + assertEquals("An Eagle in Your Mind", name.raw) + assertEquals(null, name.sort) + assertEquals("E", name.thumb) + val first = name.sortTokens[0] + assertEquals("Eagle in Your Mind", first.collationKey.sourceString) + assertEquals(SortToken.Type.LEXICOGRAPHIC, first.type) + } + + @Test + fun name_intelligent_withoutPunct_withA_withoutNumerics() { + val name = IntelligentKnownName("A Song For Our Fathers", null) + assertEquals("A Song For Our Fathers", name.raw) + assertEquals(null, name.sort) + assertEquals("S", name.thumb) + val first = name.sortTokens[0] + assertEquals("Song For Our Fathers", first.collationKey.sourceString) + assertEquals(SortToken.Type.LEXICOGRAPHIC, first.type) + } + + @Test + fun name_intelligent_withPunct_withoutArticle_withoutNumerics() { + val name = IntelligentKnownName("alt-J", null) + assertEquals("alt-J", name.raw) + assertEquals(null, name.sort) + assertEquals("A", name.thumb) + val only = name.sortTokens.single() + assertEquals("altJ", only.collationKey.sourceString) + assertEquals(SortToken.Type.LEXICOGRAPHIC, only.type) + } + + @Test + fun name_intelligent_oopsAllPunct_withoutArticle_withoutNumerics() { + val name = IntelligentKnownName("!!!", null) + assertEquals("!!!", name.raw) + assertEquals(null, name.sort) + assertEquals("!", name.thumb) + val only = name.sortTokens.single() + assertEquals("!!!", only.collationKey.sourceString) + assertEquals(SortToken.Type.LEXICOGRAPHIC, only.type) + } + + @Test + fun name_intelligent_withoutPunct_shortArticle_withNumerics() { + val name = IntelligentKnownName("the 1", null) + assertEquals("the 1", name.raw) + assertEquals(null, name.sort) + assertEquals("#", name.thumb) + val first = name.sortTokens[0] + assertEquals("1", first.collationKey.sourceString) + assertEquals(SortToken.Type.NUMERIC, first.type) + } + + @Test + fun name_intelligent_spacedPunct_withoutArticle_withoutNumerics() { + val name = IntelligentKnownName("& Yet & Yet", null) + assertEquals("& Yet & Yet", name.raw) + assertEquals(null, name.sort) + assertEquals("Y", name.thumb) + val first = name.sortTokens[0] + assertEquals("Yet Yet", first.collationKey.sourceString) + assertEquals(SortToken.Type.LEXICOGRAPHIC, first.type) + } + + @Test + fun name_intelligent_withPunct_withoutArticle_withNumerics() { + val name = IntelligentKnownName("Design : 2 : 3", null) + assertEquals("Design : 2 : 3", name.raw) + assertEquals(null, name.sort) + assertEquals("D", name.thumb) + val first = name.sortTokens[0] + assertEquals("Design", first.collationKey.sourceString) + assertEquals(SortToken.Type.LEXICOGRAPHIC, first.type) + val second = name.sortTokens[1] + assertEquals("2", second.collationKey.sourceString) + assertEquals(SortToken.Type.NUMERIC, second.type) + val third = name.sortTokens[2] + assertEquals(" ", third.collationKey.sourceString) + assertEquals(SortToken.Type.LEXICOGRAPHIC, third.type) + val fourth = name.sortTokens[3] + assertEquals("3", fourth.collationKey.sourceString) + assertEquals(SortToken.Type.NUMERIC, fourth.type) + } + + @Test + fun name_intelligent_oopsAllPunct_withoutArticle_oopsAllNumerics() { + val name = IntelligentKnownName("2 + 2 = 5", null) + assertEquals("2 + 2 = 5", name.raw) + assertEquals(null, name.sort) + assertEquals("#", name.thumb) + val first = name.sortTokens[0] + assertEquals("2", first.collationKey.sourceString) + assertEquals(SortToken.Type.NUMERIC, first.type) + val second = name.sortTokens[1] + assertEquals(" ", second.collationKey.sourceString) + assertEquals(SortToken.Type.LEXICOGRAPHIC, second.type) + val third = name.sortTokens[2] + assertEquals("2", third.collationKey.sourceString) + assertEquals(SortToken.Type.NUMERIC, third.type) + val fourth = name.sortTokens[3] + assertEquals(" ", fourth.collationKey.sourceString) + assertEquals(SortToken.Type.LEXICOGRAPHIC, fourth.type) + val fifth = name.sortTokens[4] + assertEquals("5", fifth.collationKey.sourceString) + assertEquals(SortToken.Type.NUMERIC, fifth.type) + } + + @Test + fun name_intelligent_withSort() { + val name = IntelligentKnownName("The Smile", "Smile") + assertEquals("The Smile", name.raw) + assertEquals("Smile", name.sort) + assertEquals("S", name.thumb) + val only = name.sortTokens.single() + assertEquals("Smile", only.collationKey.sourceString) + assertEquals(SortToken.Type.LEXICOGRAPHIC, only.type) + } + + @Test + fun name_equals_simple() { + val a = SimpleKnownName("The Same", "Same") + val b = SimpleKnownName("The Same", "Same") + assertEquals(a, b) + } + + @Test + fun name_equals_differentSort() { + val a = SimpleKnownName("The Same", "Same") + val b = SimpleKnownName("The Same", null) + assertNotEquals(a, b) + assertNotEquals(a.hashCode(), b.hashCode()) + } + + @Test + fun name_equals_intelligent_differentTokens() { + val a = IntelligentKnownName("The Same", "Same") + val b = IntelligentKnownName("Same", "Same") + assertNotEquals(a, b) + assertNotEquals(a.hashCode(), b.hashCode()) + } + + @Test + fun name_compareTo_simple_withoutSort_withoutArticle_withoutNumeric() { + val a = SimpleKnownName("A", null) + val b = SimpleKnownName("B", null) + assertEquals(-1, a.compareTo(b)) + } + + @Test + fun name_compareTo_simple_withoutSort_withArticle_withoutNumeric() { + val a = SimpleKnownName("A Brain in a Bottle", null) + val b = SimpleKnownName("Acid Rain", null) + val c = SimpleKnownName("Boralis / Contrastellar", null) + val d = SimpleKnownName("Breathe In", null) + assertEquals(-1, a.compareTo(b)) + assertEquals(-1, a.compareTo(c)) + assertEquals(-1, a.compareTo(d)) + } + + @Test + fun name_compareTo_simple_withSort_withoutArticle_withNumeric() { + val a = SimpleKnownName("15 Step", null) + val b = SimpleKnownName("128 Harps", null) + val c = SimpleKnownName("1969", null) + assertEquals(1, a.compareTo(b)) + assertEquals(-1, a.compareTo(c)) + } + + @Test + fun name_compareTo_simple_withPartialSort() { + val a = SimpleKnownName("A", "C") + val b = SimpleKnownName("B", null) + assertEquals(1, a.compareTo(b)) + } + + @Test + fun name_compareTo_simple_withSort() { + val a = SimpleKnownName("D", "A") + val b = SimpleKnownName("C", "B") + assertEquals(-1, a.compareTo(b)) + } + + @Test + fun name_compareTo_intelligent_withoutSort_withoutArticle_withoutNumeric() { + val a = IntelligentKnownName("A", null) + val b = IntelligentKnownName("B", null) + assertEquals(-1, a.compareTo(b)) + } + + @Test + fun name_compareTo_intelligent_withoutSort_withArticle_withoutNumeric() { + val a = IntelligentKnownName("A Brain in a Bottle", null) + val b = IntelligentKnownName("Acid Rain", null) + val c = IntelligentKnownName("Boralis / Contrastellar", null) + val d = IntelligentKnownName("Breathe In", null) + assertEquals(1, a.compareTo(b)) + assertEquals(1, a.compareTo(c)) + assertEquals(-1, a.compareTo(d)) + } + + @Test + fun name_compareTo_intelligent_withoutSort_withoutArticle_withNumeric() { + val a = IntelligentKnownName("15 Step", null) + val b = IntelligentKnownName("128 Harps", null) + val c = IntelligentKnownName("1969", null) + assertEquals(-1, a.compareTo(b)) + assertEquals(-1, b.compareTo(c)) + assertEquals(-2, a.compareTo(c)) + } + + @Test + fun name_compareTo_intelligent_withPartialSort_withoutArticle_withoutNumeric() { + val a = SimpleKnownName("A", "C") + val b = SimpleKnownName("B", null) + assertEquals(1, a.compareTo(b)) + } + + @Test + fun name_compareTo_intelligent_withSort_withoutArticle_withoutNumeric() { + val a = IntelligentKnownName("D", "A") + val b = IntelligentKnownName("C", "B") + assertEquals(-1, a.compareTo(b)) + } + + @Test + fun name_unknown() { + val a = Name.Unknown(0) + assertEquals("?", a.thumb) + } + + @Test + fun name_compareTo_mixed() { + val a = Name.Unknown(0) + val b = IntelligentKnownName("A", null) + assertEquals(-1, a.compareTo(b)) + } +} diff --git a/app/src/test/java/org/oxycblt/auxio/music/metadata/SeparatorsTest.kt b/app/src/test/java/org/oxycblt/auxio/music/metadata/SeparatorsTest.kt new file mode 100644 index 000000000..440f044e3 --- /dev/null +++ b/app/src/test/java/org/oxycblt/auxio/music/metadata/SeparatorsTest.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2023 Auxio Project + * SeparatorsTest.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.music.metadata + +import org.junit.Assert.assertEquals +import org.junit.Test + +class SeparatorsTest { + @Test + fun separators_split_withString_withSingleChar() { + assertEquals(listOf("a", "b", "c"), Separators.from(",").split(listOf("a,b,c"))) + } + + @Test + fun separators_split_withMultiple_withSingleChar() { + assertEquals(listOf("a,b", "c", "d"), Separators.from(",").split(listOf("a,b", "c", "d"))) + } + + @Test + fun separators_split_withString_withMultipleChar() { + assertEquals( + listOf("a", "b", "c", "d", "e", "f"), + Separators.from(",;/+&").split(listOf("a,b;c/d+e&f"))) + } + + @Test + fun separators_split_withList_withMultipleChar() { + assertEquals( + listOf("a,b;c/d", "e&f"), Separators.from(",;/+&").split(listOf("a,b;c/d", "e&f"))) + } +} diff --git a/app/src/test/java/org/oxycblt/auxio/music/metadata/TagUtilTest.kt b/app/src/test/java/org/oxycblt/auxio/music/metadata/TagUtilTest.kt index db340f187..7c900d42c 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/metadata/TagUtilTest.kt +++ b/app/src/test/java/org/oxycblt/auxio/music/metadata/TagUtilTest.kt @@ -20,27 +20,8 @@ package org.oxycblt.auxio.music.metadata import org.junit.Assert.assertEquals import org.junit.Test -import org.oxycblt.auxio.music.FakeMusicSettings class TagUtilTest { - @Test - fun parseMultiValue_single() { - assertEquals(listOf("a", "b", "c"), listOf("a,b,c").parseMultiValue(TestMusicSettings(","))) - } - - @Test - fun parseMultiValue_many() { - assertEquals( - listOf("a", "b", "c"), listOf("a", "b", "c").parseMultiValue(TestMusicSettings(","))) - } - - @Test - fun parseMultiValue_several() { - assertEquals( - listOf("a", "b", "c", "d", "e", "f"), - listOf("a,b;c/d+e&f").parseMultiValue(TestMusicSettings(",;/+&"))) - } - @Test fun splitEscaped_correct() { assertEquals(listOf("a", "b", "c"), "a,b,c".splitEscaped { it == ',' }) @@ -131,43 +112,30 @@ class TagUtilTest { fun parseId3v2Genre_multi() { assertEquals( listOf("Post-Rock", "Shoegaze", "Glitch"), - listOf("Post-Rock", "Shoegaze", "Glitch").parseId3GenreNames(TestMusicSettings(","))) + listOf("Post-Rock", "Shoegaze", "Glitch").parseId3GenreNames()) } @Test fun parseId3v2Genre_multiId3v1() { assertEquals( listOf("Post-Rock", "Shoegaze", "Glitch"), - listOf("176", "178", "Glitch").parseId3GenreNames(TestMusicSettings(","))) + listOf("176", "178", "Glitch").parseId3GenreNames()) } @Test fun parseId3v2Genre_wackId3() { - assertEquals(listOf("2941"), listOf("2941").parseId3GenreNames(TestMusicSettings(","))) + assertEquals(null, listOf("2941").parseId3GenreNames()) } @Test fun parseId3v2Genre_singleId3v23() { assertEquals( listOf("Post-Rock", "Shoegaze", "Remix", "Cover", "Glitch"), - listOf("(176)(178)(RX)(CR)Glitch").parseId3GenreNames(TestMusicSettings(","))) - } - - @Test - fun parseId3v2Genre_singleSeparated() { - assertEquals( - listOf("Post-Rock", "Shoegaze", "Glitch"), - listOf("Post-Rock, Shoegaze, Glitch").parseId3GenreNames(TestMusicSettings(","))) + listOf("(176)(178)(RX)(CR)Glitch").parseId3GenreNames()) } @Test fun parsId3v2Genre_singleId3v1() { - assertEquals(listOf("Post-Rock"), listOf("176").parseId3GenreNames(TestMusicSettings(","))) - } - - class TestMusicSettings(private val separators: String) : FakeMusicSettings() { - override var multiValueSeparators: String - get() = separators - set(_) = throw NotImplementedError() + assertEquals(listOf("Post-Rock"), listOf("176").parseId3GenreNames()) } } diff --git a/app/src/test/java/org/oxycblt/auxio/music/metadata/TextTagsTest.kt b/app/src/test/java/org/oxycblt/auxio/music/metadata/TextTagsTest.kt index 6cd22fdcb..9966c16e9 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/metadata/TextTagsTest.kt +++ b/app/src/test/java/org/oxycblt/auxio/music/metadata/TextTagsTest.kt @@ -39,6 +39,7 @@ class TextTagsTest { assertEquals(listOf("2022"), textTags.vorbis["date"]) assertEquals(listOf("ep"), textTags.vorbis["releasetype"]) assertEquals(listOf("+2 dB"), textTags.vorbis["replaygain_track_gain"]) + assertEquals(null, textTags.id3v2["APIC"]) } @Test @@ -51,10 +52,24 @@ class TextTagsTest { assertEquals(listOf("2022"), textTags.id3v2["TDRC"]) assertEquals(listOf("ep"), textTags.id3v2["TXXX:musicbrainz album type"]) assertEquals(listOf("+2 dB"), textTags.id3v2["TXXX:replaygain_track_gain"]) + assertEquals(null, textTags.id3v2["metadata_block_picture"]) } @Test - fun textTags_combined() { + fun textTags_mp4() { + val textTags = TextTags(MP4_METADATA) + assertTrue(textTags.vorbis.isEmpty()) + assertEquals(listOf("Wheel"), textTags.id3v2["TIT2"]) + assertEquals(listOf("Paraglow"), textTags.id3v2["TALB"]) + assertEquals(listOf("Parannoul", "Asian Glow"), textTags.id3v2["TPE1"]) + assertEquals(listOf("2022"), textTags.id3v2["TDRC"]) + assertEquals(listOf("ep"), textTags.id3v2["TXXX:musicbrainz album type"]) + assertEquals(listOf("+2 dB"), textTags.id3v2["TXXX:replaygain_track_gain"]) + assertEquals(null, textTags.id3v2["metadata_block_picture"]) + } + + @Test + fun textTags_id3v2_vorbis_combined() { val textTags = TextTags(VORBIS_METADATA.copyWithAppendedEntriesFrom(ID3V2_METADATA)) assertEquals(listOf("Wheel"), textTags.vorbis["title"]) assertEquals(listOf("Paraglow"), textTags.vorbis["album"]) @@ -62,10 +77,13 @@ class TextTagsTest { assertEquals(listOf("2022"), textTags.vorbis["date"]) assertEquals(listOf("ep"), textTags.vorbis["releasetype"]) assertEquals(listOf("+2 dB"), textTags.vorbis["replaygain_track_gain"]) + assertEquals(null, textTags.id3v2["metadata_block_picture"]) + assertEquals(listOf("Wheel"), textTags.id3v2["TIT2"]) assertEquals(listOf("Paraglow"), textTags.id3v2["TALB"]) assertEquals(listOf("Parannoul", "Asian Glow"), textTags.id3v2["TPE1"]) assertEquals(listOf("2022"), textTags.id3v2["TDRC"]) + assertEquals(null, textTags.id3v2["APIC"]) assertEquals(listOf("ep"), textTags.id3v2["TXXX:musicbrainz album type"]) assertEquals(listOf("+2 dB"), textTags.id3v2["TXXX:replaygain_track_gain"]) } @@ -90,6 +108,19 @@ class TextTagsTest { TextInformationFrame("TPE1", null, listOf("Parannoul", "Asian Glow")), TextInformationFrame("TDRC", null, listOf("2022")), TextInformationFrame("TXXX", "MusicBrainz Album Type", listOf("ep")), + TextInformationFrame("TXXX", "replaygain_track_gain", listOf("+2 dB")), + ApicFrame("", "", 0, byteArrayOf())) + + // MP4 atoms are mapped to ID3v2 text information frames by ExoPlayer, but can + // duplicate frames and have ---- mapped to InternalFrame. + private val MP4_METADATA = + Metadata( + TextInformationFrame("TIT2", null, listOf("Wheel")), + TextInformationFrame("TALB", null, listOf("Paraglow")), + TextInformationFrame("TPE1", null, listOf("Parannoul")), + TextInformationFrame("TPE1", null, listOf("Asian Glow")), + TextInformationFrame("TDRC", null, listOf("2022")), + TextInformationFrame("TXXX", "MusicBrainz Album Type", listOf("ep")), InternalFrame("com.apple.iTunes", "replaygain_track_gain", "+2 dB"), ApicFrame("", "", 0, byteArrayOf())) } diff --git a/app/src/test/java/org/oxycblt/auxio/music/user/DeviceLibraryTest.kt b/app/src/test/java/org/oxycblt/auxio/music/user/DeviceLibraryTest.kt new file mode 100644 index 000000000..cdbbc6af9 --- /dev/null +++ b/app/src/test/java/org/oxycblt/auxio/music/user/DeviceLibraryTest.kt @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2023 Auxio Project + * DeviceLibraryTest.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.music.user + +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Test +import org.oxycblt.auxio.music.Music +import org.oxycblt.auxio.music.MusicType +import org.oxycblt.auxio.music.device.AlbumImpl +import org.oxycblt.auxio.music.device.ArtistImpl +import org.oxycblt.auxio.music.device.DeviceLibraryImpl +import org.oxycblt.auxio.music.device.GenreImpl +import org.oxycblt.auxio.music.device.SongImpl + +class DeviceLibraryTest { + + @Test + fun deviceLibrary_withSongs() { + val songUidA = Music.UID.auxio(MusicType.SONGS) + val songUidB = Music.UID.auxio(MusicType.SONGS) + val songA = + mockk { + every { uid } returns songUidA + every { durationMs } returns 0 + every { finalize() } returns this + } + val songB = + mockk { + every { uid } returns songUidB + every { durationMs } returns 1 + every { finalize() } returns this + } + val deviceLibrary = DeviceLibraryImpl(listOf(songA, songB), listOf(), listOf(), listOf()) + verify { + songA.finalize() + songB.finalize() + } + val foundSongA = deviceLibrary.findSong(songUidA)!! + assertEquals(songUidA, foundSongA.uid) + assertEquals(0L, foundSongA.durationMs) + val foundSongB = deviceLibrary.findSong(songUidB)!! + assertEquals(songUidB, foundSongB.uid) + assertEquals(1L, foundSongB.durationMs) + } + + @Test + fun deviceLibrary_withAlbums() { + val albumUidA = Music.UID.auxio(MusicType.ALBUMS) + val albumUidB = Music.UID.auxio(MusicType.ALBUMS) + val albumA = + mockk { + every { uid } returns albumUidA + every { durationMs } returns 0 + every { finalize() } returns this + } + val albumB = + mockk { + every { uid } returns albumUidB + every { durationMs } returns 1 + every { finalize() } returns this + } + val deviceLibrary = DeviceLibraryImpl(listOf(), listOf(albumA, albumB), listOf(), listOf()) + verify { + albumA.finalize() + albumB.finalize() + } + val foundAlbumA = deviceLibrary.findAlbum(albumUidA)!! + assertEquals(albumUidA, foundAlbumA.uid) + assertEquals(0L, foundAlbumA.durationMs) + val foundAlbumB = deviceLibrary.findAlbum(albumUidB)!! + assertEquals(albumUidB, foundAlbumB.uid) + assertEquals(1L, foundAlbumB.durationMs) + } + + @Test + fun deviceLibrary_withArtists() { + val artistUidA = Music.UID.auxio(MusicType.ARTISTS) + val artistUidB = Music.UID.auxio(MusicType.ARTISTS) + val artistA = + mockk { + every { uid } returns artistUidA + every { durationMs } returns 0 + every { finalize() } returns this + } + val artistB = + mockk { + every { uid } returns artistUidB + every { durationMs } returns 1 + every { finalize() } returns this + } + val deviceLibrary = + DeviceLibraryImpl(listOf(), listOf(), listOf(artistA, artistB), listOf()) + verify { + artistA.finalize() + artistB.finalize() + } + val foundArtistA = deviceLibrary.findArtist(artistUidA)!! + assertEquals(artistUidA, foundArtistA.uid) + assertEquals(0L, foundArtistA.durationMs) + val foundArtistB = deviceLibrary.findArtist(artistUidB)!! + assertEquals(artistUidB, foundArtistB.uid) + assertEquals(1L, foundArtistB.durationMs) + } + + @Test + fun deviceLibrary_withGenres() { + val genreUidA = Music.UID.auxio(MusicType.GENRES) + val genreUidB = Music.UID.auxio(MusicType.GENRES) + val genreA = + mockk { + every { uid } returns genreUidA + every { durationMs } returns 0 + every { finalize() } returns this + } + val genreB = + mockk { + every { uid } returns genreUidB + every { durationMs } returns 1 + every { finalize() } returns this + } + val deviceLibrary = DeviceLibraryImpl(listOf(), listOf(), listOf(), listOf(genreA, genreB)) + verify { + genreA.finalize() + genreB.finalize() + } + val foundGenreA = deviceLibrary.findGenre(genreUidA)!! + assertEquals(genreUidA, foundGenreA.uid) + assertEquals(0L, foundGenreA.durationMs) + val foundGenreB = deviceLibrary.findGenre(genreUidB)!! + assertEquals(genreUidB, foundGenreB.uid) + assertEquals(1L, foundGenreB.durationMs) + } + + @Test + fun deviceLibrary_equals() { + val songA = + mockk { + every { uid } returns Music.UID.auxio(MusicType.SONGS) + every { finalize() } returns this + } + val songB = + mockk { + every { uid } returns Music.UID.auxio(MusicType.SONGS) + every { finalize() } returns this + } + val album = + mockk { + every { uid } returns mockk() + every { finalize() } returns this + } + + val deviceLibraryA = DeviceLibraryImpl(listOf(songA), listOf(album), listOf(), listOf()) + val deviceLibraryB = DeviceLibraryImpl(listOf(songA), listOf(), listOf(), listOf()) + val deviceLibraryC = DeviceLibraryImpl(listOf(songB), listOf(album), listOf(), listOf()) + assertEquals(deviceLibraryA, deviceLibraryB) + assertEquals(deviceLibraryA.hashCode(), deviceLibraryA.hashCode()) + assertNotEquals(deviceLibraryA, deviceLibraryC) + assertNotEquals(deviceLibraryA.hashCode(), deviceLibraryC.hashCode()) + } +} diff --git a/app/src/test/java/org/oxycblt/auxio/util/TestingUtil.kt b/app/src/test/java/org/oxycblt/auxio/util/TestingUtil.kt deleted file mode 100644 index 5da90ab54..000000000 --- a/app/src/test/java/org/oxycblt/auxio/util/TestingUtil.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (c) 2023 Auxio Project - * TestingUtil.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.util - -import androidx.lifecycle.ViewModel - -private val VM_CLEAR_METHOD = - ViewModel::class.java.getDeclaredMethod("clear").apply { isAccessible = true } - -fun ViewModel.forceClear() { - VM_CLEAR_METHOD.invoke(this) -} diff --git a/build.gradle b/build.gradle index 8713d9839..e2f1717dc 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - kotlin_version = '1.9.0' + kotlin_version = '1.9.10' navigation_version = "2.5.3" hilt_version = '2.47' } @@ -12,10 +12,10 @@ buildscript { } plugins { - id "com.android.application" version "8.1.0" apply false + id "com.android.application" version '8.1.2' apply false id "androidx.navigation.safeargs.kotlin" version "$navigation_version" apply false id "org.jetbrains.kotlin.android" version "$kotlin_version" apply false - id "com.google.devtools.ksp" version '1.9.0-1.0.12' apply false + id "com.google.devtools.ksp" version '1.9.10-1.0.13' apply false id "com.diffplug.spotless" version "6.20.0" apply false } diff --git a/fastlane/metadata/android/en-US/changelogs/36.txt b/fastlane/metadata/android/en-US/changelogs/36.txt new file mode 100644 index 000000000..b0ac8dc87 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/36.txt @@ -0,0 +1,3 @@ +Auxio 3.2.0 refreshes the item management experience, with a new menu UI and playback options. +This release fixes several critical issues identified in the previous version. +For more information, see https://github.com/OxygenCobalt/Auxio/releases/tag/v3.2.0. \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index b43251ee6..3f4927359 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -20,4 +20,4 @@ precise/original dates, sort tags, and more - Headset autoplay - Stylish widgets that automatically adapt to their size - Completely private and offline -- No rounded album covers (Unless you want them. Then you can.) \ No newline at end of file +- No rounded album covers (by default) \ No newline at end of file diff --git a/fastlane/metadata/android/fr-FR/full_description.txt b/fastlane/metadata/android/fr-FR/full_description.txt new file mode 100644 index 000000000..107936eaf --- /dev/null +++ b/fastlane/metadata/android/fr-FR/full_description.txt @@ -0,0 +1,23 @@ +Auxio est un lecteur de musique local doté d'une UI/UX rapide et sûre, sans les fonctions inutiles de la plupart des autres lecteurs. Construit sur les bases d'une librairie moderne de lecture de media, Auxio supporte une libaririe et propose une qualité d'écoute supérieurs comparé aux autres applications qui utilisent des fonctionnalités d'android dépassées. Pour faire simple, il joue votre musique . + +Fonctionnalités + +- Lecture basée sur l'ExoPlayer Media3 +- UI réactive dérivée des dernières lignes directrices en Material Design +- UX orientée qui mets l'accent sur la facilité d'utilisation plutôt que sur les usages +- Comportement personnalisable +- Reconnaît les numéros de disque, les artistes multiples, les types de support, +les dates précises/originales, le classement par tags, and plus encore +- Système de reconaissance d'artistes avancé qui unifie artistes et artistes de l'album +- Carte SD reconnue par le système de dossiers +- Fonction de liste de lecture efficace +- Statut de lecture persistant +- Support complet de ReplayGain (pour les fichiers MP3, FLAC, OGG, OPUS, et MP4) +- Support pour égaliseur externe (ex. Wavelet) +- Navigation bord-à-bord +- Couvertures intégrées reconnues +- Recherche intégrée +- Lecture automatique pour les casques +- Widgets stylisés qui s'adaptent automatiquement à leur taille +- Complètement privé et hors-ligne +- On arrondit pas les couvertures d'albums (Sauf si vous le voulez. Dans ce cas c'est possible.) diff --git a/fastlane/metadata/android/he/full_description.txt b/fastlane/metadata/android/he/full_description.txt index 588fec696..1cadc6eb3 100644 --- a/fastlane/metadata/android/he/full_description.txt +++ b/fastlane/metadata/android/he/full_description.txt @@ -6,10 +6,9 @@ - ממשק משתמש מהיר שנגזר מהנחיות Material Design האחרונות ביותר - חוויית משתמש שמתעדפת נוחות שימוש על פני מקרי קיצון - התנהגות מותאמת אישית -- תמיכה במספרי דיסק, אומנים מרובים, סוגי שחרור, +- תמיכה במספרי דיסק, אומנים מרובים, סוגי שחרור, תאריכים מדוייקים/מקוריים, תגיות מיון, ועוד - מערכת אומנים מתקדמת שמאחדת אומנים ואומני אלבום - - ניהול תיקיות מודע לכרטיסי SD - פונקציונליות פלייליסטים אמינה - התמדה במצב ההשמעה @@ -21,5 +20,4 @@ - ניגון אוטומטי באוזניות - ווידג'טים אלגנטיים שמתאימים את עצמם לגודלם אוטומטית - פרטי לגמרי ולא מקוון - -- ללא עטיפות אלבום מעוגלות (אלא אם את.ה מעוניינ.ת בהם. אחרת אפשר.) +- ללא עטיפות אלבום מעוגלות (אלא אם את.ה מעוניינ.ת בהם. אז זה אפשרי.) diff --git a/fastlane/metadata/android/pt-PT/full_description.txt b/fastlane/metadata/android/pt-PT/full_description.txt new file mode 100644 index 000000000..65e70b61e --- /dev/null +++ b/fastlane/metadata/android/pt-PT/full_description.txt @@ -0,0 +1,21 @@ +Auxio é um leitor de música local com uma UI/UX rápida e fiável sem as muitas funcionalidades inúteis presentes noutros leitores de música. Construído a partir de bibliotecas de reprodução de mídia modernas, Auxio tem suporte de biblioteca superior e qualidade de audição em comparação com outras aplicações que usam funcionalidade Android desatualizadas. Em suma, toca música. + +Caraterísticas + +- Reprodução baseada em Media3 ExoPlayer +- Snappy UI derivada das mais recentes diretrizes de Material Design +- UX opinativa que prioriza a facilidade de uso sobre casos de borda +- Comportamento personalizável +- Suporte para números de disco, vários artistas, tipos de lançamento, +datas precisas/originais, tags de classificação e muito mais +- Sistema avançado de artistas que unifica artistas e artistas de álbuns +- Gerenciamento de pastas com reconhecimento de cartão SD +- Funcionalidade de playlisting confiável +- Persistência do estado de reprodução +- Suporte completo ReplayGain (em arquivos MP3, FLAC, OGG, OPUS e MP4) +- Suporte de equalizador externo (ex. Wavelet) +- De ponta a ponta +- Suporte de capas embutidas +- Funcionalidade de pesquisa +- Reprodução automática de auscultadores +- Elegante diff --git a/fastlane/metadata/android/pt-PT/short_description.txt b/fastlane/metadata/android/pt-PT/short_description.txt new file mode 100644 index 000000000..afb175103 --- /dev/null +++ b/fastlane/metadata/android/pt-PT/short_description.txt @@ -0,0 +1 @@ +Um leitor de música simples diff --git a/fastlane/metadata/android/sl/full_description.txt b/fastlane/metadata/android/sl/full_description.txt new file mode 100644 index 000000000..c8ee9819d --- /dev/null +++ b/fastlane/metadata/android/sl/full_description.txt @@ -0,0 +1,22 @@ +Auxio je lokalni predvajalnik glasbe z hitrim in zanesljivim uporabniškim vmesnikom brez večino nepotrebnih funkcij, ki jih najdete v drugih predvajalnikih glasbe. Zgrajen na sodobnih knjižnicah za predvajanje medijskih vsebin, Auxio ponuja izjemno podporo za knjižnico in kakovost poslušanja v primerjavi z aplikacijami, ki uporabljajo zastarelo funkcionalnost Androida. Skratka, predvaja glasbo. + +Lastnosti + +- Predvajanje temelji na Media3 ExoPlayer predvajalniku +- Hiter uporabniški vmesnik, izpeljan iz najnovejših smernic oblikovanja gradiva (Material Design) +- Samostojno premišljena uporabniška izkušnja, ki postavlja enostavnost uporabe pred izjemne primere, ki se zelo redko zgodijo +- Prilagodljivo obnašanje +- Podpora za številke diskov, več izvajalcev, vrste izdaj, natančne/izvirne datume, razvrščalne oznake in še več +- Napreden sistem izvajalcev, ki združuje izvajalce in izvajalce albumov +- Upravljanje map na SD kartici +- Zanesljiva funkcionalnost ustvarjanja seznama predvajanja +- Trajnost stanja predvajanja +- Popolna podpora za ReplayGain tehnologijo (za MP3, FLAC, OGG, OPUS in MP4 datoteke) +- Podpora za zunanje izenačevalnike (npr. Wavelet) +- Od roba do roba +- Podpora za vdelane naslovnice albumov +- Funkcionalnost iskanja +- Avtomatski zagon ob priključitvi slušalk +- Elegantni pripomočki, ki se samodejno prilagajajo svoji velikosti +- Popolnoma zasebno in brez povezave +- Brez zaobljenih naslovnic albumov (če jih ne želite; če pa želite, jih lahko omogočite) diff --git a/fastlane/metadata/android/sl/short_description.txt b/fastlane/metadata/android/sl/short_description.txt new file mode 100644 index 000000000..568819de9 --- /dev/null +++ b/fastlane/metadata/android/sl/short_description.txt @@ -0,0 +1 @@ +Preprost, racionalen predvajalnik glasbe diff --git a/fastlane/metadata/android/tr/full_description.txt b/fastlane/metadata/android/tr/full_description.txt index e5b9134cf..86460ef08 100644 --- a/fastlane/metadata/android/tr/full_description.txt +++ b/fastlane/metadata/android/tr/full_description.txt @@ -1,19 +1,23 @@ -Auxio, diğer müzik oynatıcılarda bulunan birçok gereksiz özellik olmadan hızlı, güvenilir bir kullanıcı arayüzüne ve deneyimine sahip yerel bir müzik çalardır. <a href="https://exoplayer.dev/">Exoplayer</a> üzerine inşa edilen Auxio, yerel MediaPlayer API'sini kullanan diğer uygulamalara kıyasla çok daha iyi bir dinleme deneyimine sahiptir. Kısaca, Müzik çalar. +Auxio, diğer müzik çalarlarda bulunan birçok gereksiz özellik olmadan hızlı, güvenilir bir UI / UX'a sahip yerel bir müzik oynatıcıdır. Modern medya oynatma kütüphaneleri üzerine inşa edilen Auxio, eski android işlevselliğini kullanan diğer uygulamalara kıyasla üstün kütüphane desteği ve dinleme kalitesine sahiptir. Kısacası, Müzik çalar. Özellikler -- ExoPlayer tabanlı oynatma +- Media3 ExoPlayer tabanlı oynatma - En son Materyal Tasarım yönergelerinden türetilen hızlı kullanıcı arayüzü - Uç durumlardan ziyade kullanım kolaylığına öncelik veren fikir sahibi kullanıcı deneyimi - Özelleştirilebilir davranış -- Doğru meta verilere öncelik veren gelişmiş medya indeksleyici +- Disk numaraları, çoklu sanatçılar, sürüm türleri için destek, +kesin/orijinal tarihler, sıralama etiketleri ve daha fazlası +- Sanatçıları ve albüm sanatçılarını birleştiren gelişmiş sanatçı sistemi - SD Card-aware klasör yönetimi -- Güvenilir oynatma durumu kalıcılığı -- Tam ReplayGain desteği (MP3, MP4, FLAC, OGG ve OPUS'ta) +- Güvenilir çalma listesi işlevi +- Oynatma durumu kalıcılığı +- Tam ReplayGain desteği (MP3, FLAC, OGG, OPUS ve MP4 dosyalarında) +- Harici ekolayzer desteği (örn. Wavelet) - Kenardan kenara - Gömülü kapak desteği -- Arama İşlevselliği +- Arama işlevi - Kulaklık otomatik oynatma - Boyutlarına otomatik olarak uyum sağlayan şık widget'lar - Tamamen özel ve çevrimdışı -- Yuvarlak albüm kapakları yok (İstediğiniz zaman açıp kapatabilirsiniz.) +- Yuvarlak albüm kapakları yok (İstemediğiniz sürece. O zaman yapabilirsiniz.)