Merge pull request #480 from OxygenCobalt/dev

Version 3.1.2
This commit is contained in:
Alexander Capehart 2023-06-17 09:15:14 -06:00 committed by GitHub
commit 81b030bfec
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 1217 additions and 620 deletions

View file

@ -1,5 +1,20 @@
# Changelog # Changelog
## 3.1.2
#### What's Improved
- `artistssort`, `albumartistssort`, and `album_artists` tags are now recognized
- Non-english digit strings are sorted more correctly
- Reduced visual loading time
- Genre/artist/album information is now obtained by specific child items
#### What's Fixed
- Disc number is no longer mis-aligned when no subtitle is present
- Fixed selection not updating when playlists are changed
- Fixed duplicate albums appearing in certain cases
- Fixed ReplayGain adjustment not applying at the start of a song in certain cases
- Music cache is no longer migrated between devices
## 3.1.1 ## 3.1.1
#### What's New #### What's New

View file

@ -2,8 +2,8 @@
<h1 align="center"><b>Auxio</b></h1> <h1 align="center"><b>Auxio</b></h1>
<h4 align="center">A simple, rational music player for android.</h4> <h4 align="center">A simple, rational music player for android.</h4>
<p align="center"> <p align="center">
<a href="https://github.com/oxygencobalt/Auxio/releases/tag/v3.1.1"> <a href="https://github.com/oxygencobalt/Auxio/releases/tag/v3.1.2">
<img alt="Latest Version" src="https://img.shields.io/static/v1?label=tag&message=v3.1.1&color=64B5F6&style=flat"> <img alt="Latest Version" src="https://img.shields.io/static/v1?label=tag&message=v3.1.2&color=64B5F6&style=flat">
</a> </a>
<a href="https://github.com/oxygencobalt/Auxio/releases/"> <a href="https://github.com/oxygencobalt/Auxio/releases/">
<img alt="Releases" src="https://img.shields.io/github/downloads/OxygenCobalt/Auxio/total.svg?color=4B95DE&style=flat"> <img alt="Releases" src="https://img.shields.io/github/downloads/OxygenCobalt/Auxio/total.svg?color=4B95DE&style=flat">

View file

@ -20,8 +20,8 @@ android {
defaultConfig { defaultConfig {
applicationId namespace applicationId namespace
versionName "3.1.1" versionName "3.1.2"
versionCode 31 versionCode 32
minSdk 24 minSdk 24
targetSdk 33 targetSdk 33
@ -56,6 +56,7 @@ android {
} }
} }
} }
packagingOptions { packagingOptions {
jniLibs { jniLibs {
excludes += ['**/kotlin/**', '**/okhttp3/**'] excludes += ['**/kotlin/**', '**/okhttp3/**']
@ -65,7 +66,6 @@ android {
} }
} }
buildFeatures { buildFeatures {
viewBinding true viewBinding true
} }

View file

@ -24,12 +24,12 @@
android:name=".Auxio" android:name=".Auxio"
android:allowBackup="true" android:allowBackup="true"
android:fullBackupContent="@xml/backup_descriptor" android:fullBackupContent="@xml/backup_descriptor"
android:dataExtractionRules="@xml/data_extraction_rules"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/info_app_name" android:label="@string/info_app_name"
android:roundIcon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.Auxio.App" android:theme="@style/Theme.Auxio.App"
android:dataExtractionRules="@xml/data_extraction_rules"
android:appCategory="audio" android:appCategory="audio"
android:enableOnBackInvokedCallback="true" android:enableOnBackInvokedCallback="true"
tools:ignore="UnusedAttribute"> tools:ignore="UnusedAttribute">

View file

@ -149,7 +149,7 @@ class MainFragment :
logD("Configuring stacked bottom sheets") logD("Configuring stacked bottom sheets")
unlikelyToBeNull(binding.queueHandleWrapper).setOnClickListener { unlikelyToBeNull(binding.queueHandleWrapper).setOnClickListener {
if (playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED && if (playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED &&
queueSheetBehavior.state == BackportBottomSheetBehavior.STATE_COLLAPSED) { queueSheetBehavior.targetState == BackportBottomSheetBehavior.STATE_COLLAPSED) {
// Playback sheet is expanded and queue sheet is collapsed, we can expand it. // Playback sheet is expanded and queue sheet is collapsed, we can expand it.
queueSheetBehavior.state = BackportBottomSheetBehavior.STATE_EXPANDED queueSheetBehavior.state = BackportBottomSheetBehavior.STATE_EXPANDED
} }
@ -414,7 +414,7 @@ class MainFragment :
val playbackSheetBehavior = val playbackSheetBehavior =
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
if (playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_COLLAPSED) { if (playbackSheetBehavior.targetState == BackportBottomSheetBehavior.STATE_COLLAPSED) {
// Playback sheet is not expanded and not hidden, we can expand it. // Playback sheet is not expanded and not hidden, we can expand it.
logD("Expanding playback sheet") logD("Expanding playback sheet")
playbackSheetBehavior.state = BackportBottomSheetBehavior.STATE_EXPANDED playbackSheetBehavior.state = BackportBottomSheetBehavior.STATE_EXPANDED
@ -424,9 +424,9 @@ class MainFragment :
val queueSheetBehavior = val queueSheetBehavior =
(binding.queueSheet.coordinatorLayoutBehavior ?: return) as QueueBottomSheetBehavior (binding.queueSheet.coordinatorLayoutBehavior ?: return) as QueueBottomSheetBehavior
if (playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED && if (playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED &&
queueSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED) { queueSheetBehavior.targetState == BackportBottomSheetBehavior.STATE_EXPANDED) {
// Queue sheet and playback sheet is expanded, close the queue sheet so the // Queue sheet and playback sheet is expanded, close the queue sheet so the
// playback panel can eb shown. // playback panel can shown.
logD("Collapsing queue sheet") logD("Collapsing queue sheet")
queueSheetBehavior.state = BackportBottomSheetBehavior.STATE_COLLAPSED queueSheetBehavior.state = BackportBottomSheetBehavior.STATE_COLLAPSED
} }
@ -436,7 +436,7 @@ class MainFragment :
val binding = requireBinding() val binding = requireBinding()
val playbackSheetBehavior = val playbackSheetBehavior =
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
if (playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED) { if (playbackSheetBehavior.targetState == BackportBottomSheetBehavior.STATE_EXPANDED) {
// Playback sheet (and possibly queue) needs to be collapsed. // Playback sheet (and possibly queue) needs to be collapsed.
logD("Collapsing playback and queue sheets") logD("Collapsing playback and queue sheets")
val queueSheetBehavior = val queueSheetBehavior =
@ -487,8 +487,6 @@ class MainFragment :
} }
} }
// TODO: Use targetState more
private class SheetBackPressedCallback( private class SheetBackPressedCallback(
private val playbackSheetBehavior: PlaybackBottomSheetBehavior<*>, private val playbackSheetBehavior: PlaybackBottomSheetBehavior<*>,
private val queueSheetBehavior: QueueBottomSheetBehavior<*>? private val queueSheetBehavior: QueueBottomSheetBehavior<*>?

View file

@ -480,7 +480,7 @@ constructor(
// implicit album list into the mapping. // implicit album list into the mapping.
logD("Implicit albums present, adding to list") logD("Implicit albums present, adding to list")
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
(grouping as MutableMap<AlbumGrouping, List<Album>>)[AlbumGrouping.APPEARANCES] = (grouping as MutableMap<AlbumGrouping, Collection<Album>>)[AlbumGrouping.APPEARANCES] =
artist.implicitAlbums artist.implicitAlbums
} }
@ -490,7 +490,7 @@ constructor(
val header = BasicHeader(entry.key.headerTitleRes) val header = BasicHeader(entry.key.headerTitleRes)
list.add(Divider(header)) list.add(Divider(header))
list.add(header) list.add(header)
list.addAll(entry.value) list.addAll(ARTIST_ALBUM_SORT.albums(entry.value))
} }
// Artists may not be linked to any songs, only include a header entry if we have any. // Artists may not be linked to any songs, only include a header entry if we have any.
@ -519,7 +519,7 @@ constructor(
val artistHeader = BasicHeader(R.string.lbl_artists) val artistHeader = BasicHeader(R.string.lbl_artists)
list.add(Divider(artistHeader)) list.add(Divider(artistHeader))
list.add(artistHeader) list.add(artistHeader)
list.addAll(genre.artists) list.addAll(GENRE_ARTIST_SORT.artists(genre.artists))
val songHeader = SortHeader(R.string.lbl_songs) val songHeader = SortHeader(R.string.lbl_songs)
list.add(Divider(songHeader)) list.add(Divider(songHeader))
@ -576,4 +576,9 @@ constructor(
LIVE(R.string.lbl_live_group), LIVE(R.string.lbl_live_group),
REMIXES(R.string.lbl_remix_group), REMIXES(R.string.lbl_remix_group),
} }
private companion object {
val ARTIST_ALBUM_SORT = Sort(Sort.Mode.ByDate, Sort.Direction.DESCENDING)
val GENRE_ARTIST_SORT = Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING)
}
} }

View file

@ -115,7 +115,7 @@ private class DiscHeaderViewHolder(private val binding: ItemDiscHeaderBinding) :
binding.discNumber.text = binding.context.getString(R.string.fmt_disc_no, disc.number) binding.discNumber.text = binding.context.getString(R.string.fmt_disc_no, disc.number)
binding.discName.apply { binding.discName.apply {
text = disc.name text = disc.name
isGone = text == null isGone = disc.name == null
} }
} else { } else {
logD("Disc is null, defaulting to no disc") logD("Disc is null, defaulting to no disc")

View file

@ -52,6 +52,7 @@ import org.oxycblt.auxio.home.list.GenreListFragment
import org.oxycblt.auxio.home.list.PlaylistListFragment import org.oxycblt.auxio.home.list.PlaylistListFragment
import org.oxycblt.auxio.home.list.SongListFragment import org.oxycblt.auxio.home.list.SongListFragment
import org.oxycblt.auxio.home.tabs.AdaptiveTabStrategy import org.oxycblt.auxio.home.tabs.AdaptiveTabStrategy
import org.oxycblt.auxio.home.tabs.Tab
import org.oxycblt.auxio.list.Sort import org.oxycblt.auxio.list.Sort
import org.oxycblt.auxio.list.selection.SelectionFragment import org.oxycblt.auxio.list.selection.SelectionFragment
import org.oxycblt.auxio.list.selection.SelectionViewModel import org.oxycblt.auxio.list.selection.SelectionViewModel
@ -152,9 +153,10 @@ class HomeFragment :
setOnApplyWindowInsetsListener { _, insets -> insets } setOnApplyWindowInsetsListener { _, insets -> insets }
// We know that there will only be a fixed amount of tabs, so we manually set this // We know that there will only be a fixed amount of tabs, so we manually set this
// limit to that. This also prevents the appbar lift state from being confused during // limit to the maximum amount possible. This will prevent the tab ripple from
// page transitions. // bugging out due to dynamically inflating each fragment, at the cost of slower
offscreenPageLimit = homeModel.currentTabModes.size // debug UI performance.
offscreenPageLimit = Tab.MAX_SEQUENCE_IDX + 1
// By default, ViewPager2's sensitivity is high enough to result in vertical scroll // By default, ViewPager2's sensitivity is high enough to result in vertical scroll
// events being registered as horizontal scroll events. Reflect into the internal // events being registered as horizontal scroll events. Reflect into the internal

View file

@ -59,7 +59,7 @@ sealed class Tab(open val mode: MusicMode) {
// MusicMode for this tab. // MusicMode for this tab.
/** The maximum index that a well-formed tab sequence should be. */ /** The maximum index that a well-formed tab sequence should be. */
private const val MAX_SEQUENCE_IDX = 4 const val MAX_SEQUENCE_IDX = 4
/** /**
* The default tab sequence, in integer form. This represents a set of four visible tabs * The default tab sequence, in integer form. This represents a set of four visible tabs

View file

@ -379,7 +379,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
* @param desc The content description to describe the bound data. * @param desc The content description to describe the bound data.
* @param errorRes The resource of the error drawable to use if the cover cannot be loaded. * @param errorRes The resource of the error drawable to use if the cover cannot be loaded.
*/ */
fun bind(songs: List<Song>, desc: String, @DrawableRes errorRes: Int) { fun bind(songs: Collection<Song>, desc: String, @DrawableRes errorRes: Int) {
val request = val request =
ImageRequest.Builder(context) ImageRequest.Builder(context)
.data(songs) .data(songs)

View file

@ -27,22 +27,22 @@ import javax.inject.Inject
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
class SongKeyer @Inject constructor(private val coverExtractor: CoverExtractor) : class SongKeyer @Inject constructor(private val coverExtractor: CoverExtractor) :
Keyer<List<Song>> { Keyer<Collection<Song>> {
override fun key(data: List<Song>, options: Options) = override fun key(data: Collection<Song>, options: Options) =
"${coverExtractor.computeCoverOrdering(data).hashCode()}" "${coverExtractor.computeCoverOrdering(data).hashCode()}"
} }
class SongCoverFetcher class SongCoverFetcher
private constructor( private constructor(
private val songs: List<Song>, private val songs: Collection<Song>,
private val size: Size, private val size: Size,
private val coverExtractor: CoverExtractor, private val coverExtractor: CoverExtractor,
) : Fetcher { ) : Fetcher {
override suspend fun fetch() = coverExtractor.extract(songs, size) override suspend fun fetch() = coverExtractor.extract(songs, size)
class Factory @Inject constructor(private val coverExtractor: CoverExtractor) : class Factory @Inject constructor(private val coverExtractor: CoverExtractor) :
Fetcher.Factory<List<Song>> { Fetcher.Factory<Collection<Song>> {
override fun create(data: List<Song>, options: Options, imageLoader: ImageLoader) = override fun create(data: Collection<Song>, options: Options, imageLoader: ImageLoader) =
SongCoverFetcher(data, options.size, coverExtractor) SongCoverFetcher(data, options.size, coverExtractor)
} }
} }

View file

@ -77,7 +77,7 @@ constructor(
* will be returned of a mosaic composed of four album covers ordered by * will be returned of a mosaic composed of four album covers ordered by
* [computeCoverOrdering]. Otherwise, a [SourceResult] of one album cover will be returned. * [computeCoverOrdering]. Otherwise, a [SourceResult] of one album cover will be returned.
*/ */
suspend fun extract(songs: List<Song>, size: Size): FetchResult? { suspend fun extract(songs: Collection<Song>, size: Size): FetchResult? {
val albums = computeCoverOrdering(songs) val albums = computeCoverOrdering(songs)
val streams = mutableListOf<InputStream>() val streams = mutableListOf<InputStream>()
for (album in albums) { for (album in albums) {
@ -117,7 +117,7 @@ constructor(
* by their names. "Representation" is defined by how many [Song]s were found to be linked to * by their names. "Representation" is defined by how many [Song]s were found to be linked to
* the given [Album] in the given [Song] list. * the given [Album] in the given [Song] list.
*/ */
fun computeCoverOrdering(songs: List<Song>): List<Album> { fun computeCoverOrdering(songs: Collection<Song>): List<Album> {
// TODO: Start short-circuiting in more places // TODO: Start short-circuiting in more places
if (songs.isEmpty()) return listOf() if (songs.isEmpty()) return listOf()
if (songs.size == 1) return listOf(songs.first().album) if (songs.size == 1) return listOf(songs.first().album)
@ -150,7 +150,7 @@ constructor(
MediaMetadataRetriever().run { MediaMetadataRetriever().run {
// This call is time-consuming but it also doesn't seem to hold up the main thread, // This call is time-consuming but it also doesn't seem to hold up the main thread,
// so it's probably fine not to wrap it.rmt // so it's probably fine not to wrap it.rmt
setDataSource(context, album.songs[0].uri) setDataSource(context, album.coverUri.song)
// Get the embedded picture from MediaMetadataRetriever, which will return a full // Get the embedded picture from MediaMetadataRetriever, which will return a full
// ByteArray of the cover without any compression artifacts. // ByteArray of the cover without any compression artifacts.
@ -161,7 +161,7 @@ constructor(
private suspend fun extractExoplayerCover(album: Album): InputStream? { private suspend fun extractExoplayerCover(album: Album): InputStream? {
val tracks = val tracks =
MetadataRetriever.retrieveMetadata( MetadataRetriever.retrieveMetadata(
mediaSourceFactory, MediaItem.fromUri(album.songs[0].uri)) mediaSourceFactory, MediaItem.fromUri(album.coverUri.song))
.asDeferred() .asDeferred()
.await() .await()
@ -207,7 +207,9 @@ constructor(
private suspend fun extractMediaStoreCover(album: Album) = private suspend fun extractMediaStoreCover(album: Album) =
// Eliminate any chance that this blocking call might mess up the loading process // Eliminate any chance that this blocking call might mess up the loading process
withContext(Dispatchers.IO) { context.contentResolver.openInputStream(album.coverUri) } withContext(Dispatchers.IO) {
context.contentResolver.openInputStream(album.coverUri.mediaStore)
}
/** Derived from phonograph: https://github.com/kabouzeid/Phonograph */ /** Derived from phonograph: https://github.com/kabouzeid/Phonograph */
private fun createMosaic(streams: List<InputStream>, size: Size): FetchResult { private fun createMosaic(streams: List<InputStream>, size: Size): FetchResult {

View file

@ -0,0 +1,32 @@
/*
* Copyright (c) 2023 Auxio Project
* CoverUri.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.image.extractor
import android.net.Uri
/**
* Bundle of [Uri] information used in [CoverExtractor] to ensure consistent [Uri] use when loading
* images.
*
* @param mediaStore The album cover [Uri] obtained from MediaStore.
* @param song The [Uri] of the first song (by track) of the album, which can also be used to obtain
* an album cover.
* @author Alexander Capehart (OxygenCobalt)
*/
data class CoverUri(val mediaStore: Uri, val song: Uri)

View file

@ -56,7 +56,6 @@ constructor(
} }
override fun onMusicChanges(changes: MusicRepository.Changes) { override fun onMusicChanges(changes: MusicRepository.Changes) {
if (!changes.deviceLibrary) return
val deviceLibrary = musicRepository.deviceLibrary ?: return val deviceLibrary = musicRepository.deviceLibrary ?: return
val userLibrary = musicRepository.userLibrary ?: return val userLibrary = musicRepository.userLibrary ?: return
// Sanitize the selection to remove items that no longer exist and thus // Sanitize the selection to remove items that no longer exist and thus

View file

@ -27,6 +27,7 @@ import java.util.UUID
import kotlin.math.max import kotlin.math.max
import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.oxycblt.auxio.image.extractor.CoverUri
import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.music.fs.MimeType import org.oxycblt.auxio.music.fs.MimeType
import org.oxycblt.auxio.music.fs.Path import org.oxycblt.auxio.music.fs.Path
@ -34,6 +35,7 @@ import org.oxycblt.auxio.music.info.Date
import org.oxycblt.auxio.music.info.Disc import org.oxycblt.auxio.music.info.Disc
import org.oxycblt.auxio.music.info.Name import org.oxycblt.auxio.music.info.Name
import org.oxycblt.auxio.music.info.ReleaseType import org.oxycblt.auxio.music.info.ReleaseType
import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment
import org.oxycblt.auxio.util.concatLocalized import org.oxycblt.auxio.util.concatLocalized
import org.oxycblt.auxio.util.toUuidOrNull import org.oxycblt.auxio.util.toUuidOrNull
@ -224,7 +226,7 @@ sealed interface Music : Item {
*/ */
sealed interface MusicParent : Music { sealed interface MusicParent : Music {
/** The child [Song]s of this [MusicParent]. */ /** The child [Song]s of this [MusicParent]. */
val songs: List<Song> val songs: Collection<Song>
} }
/** /**
@ -255,6 +257,8 @@ interface Song : Music {
val size: Long val size: Long
/** The duration of the audio file, in milliseconds. */ /** The duration of the audio file, in milliseconds. */
val durationMs: Long val durationMs: Long
/** The ReplayGain adjustment to apply during playback. */
val replayGainAdjustment: ReplayGainAdjustment
/** The date the audio file was added to the device, as a unix epoch timestamp. */ /** The date the audio file was added to the device, as a unix epoch timestamp. */
val dateAdded: Long val dateAdded: Long
/** /**
@ -293,7 +297,7 @@ interface Album : MusicParent {
* The URI to a MediaStore-provided album cover. These images will be fast to load, but at the * The URI to a MediaStore-provided album cover. These images will be fast to load, but at the
* cost of image quality. * cost of image quality.
*/ */
val coverUri: Uri val coverUri: CoverUri
/** The duration of all songs in the album, in milliseconds. */ /** The duration of all songs in the album, in milliseconds. */
val durationMs: Long val durationMs: Long
/** The earliest date a song in this album was added, as a unix epoch timestamp. */ /** The earliest date a song in this album was added, as a unix epoch timestamp. */
@ -318,14 +322,11 @@ interface Artist : MusicParent {
* Note that any [Song] credited to this artist will have it's [Album] considered to be * Note that any [Song] credited to this artist will have it's [Album] considered to be
* "indirectly" linked to this [Artist], and thus included in this list. * "indirectly" linked to this [Artist], and thus included in this list.
*/ */
val albums: List<Album> val albums: Collection<Album>
/** Albums directly credited to this [Artist] via a "Album Artist" tag. */ /** Albums directly credited to this [Artist] via a "Album Artist" tag. */
val explicitAlbums: List<Album> val explicitAlbums: Collection<Album>
/** Albums indirectly credited to this [Artist] via an "Artist" tag. */ /** Albums indirectly credited to this [Artist] via an "Artist" tag. */
val implicitAlbums: List<Album> val implicitAlbums: Collection<Album>
/** /**
* The duration of all [Song]s in the artist, in milliseconds. Will be null if there are no * The duration of all [Song]s in the artist, in milliseconds. Will be null if there are no
* songs. * songs.
@ -342,7 +343,7 @@ interface Artist : MusicParent {
*/ */
interface Genre : MusicParent { interface Genre : MusicParent {
/** The artists indirectly linked to by the [Artist]s of this [Genre]. */ /** The artists indirectly linked to by the [Artist]s of this [Genre]. */
val artists: List<Artist> val artists: Collection<Artist>
/** The total duration of the songs in this genre, in milliseconds. */ /** The total duration of the songs in this genre, in milliseconds. */
val durationMs: Long val durationMs: Long
} }
@ -353,6 +354,7 @@ interface Genre : MusicParent {
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
interface Playlist : MusicParent { interface Playlist : MusicParent {
override val songs: List<Song>
/** The total duration of the songs in this genre, in milliseconds. */ /** The total duration of the songs in this genre, in milliseconds. */
val durationMs: Long val durationMs: Long
} }

View file

@ -32,6 +32,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import org.oxycblt.auxio.music.cache.CacheRepository import org.oxycblt.auxio.music.cache.CacheRepository
import org.oxycblt.auxio.music.device.DeviceLibrary import org.oxycblt.auxio.music.device.DeviceLibrary
@ -300,35 +301,35 @@ constructor(
val userLibrary = synchronized(this) { userLibrary ?: return } val userLibrary = synchronized(this) { userLibrary ?: return }
logD("Creating playlist $name with ${songs.size} songs") logD("Creating playlist $name with ${songs.size} songs")
userLibrary.createPlaylist(name, songs) userLibrary.createPlaylist(name, songs)
emitLibraryChange(device = false, user = true) dispatchLibraryChange(device = false, user = true)
} }
override suspend fun renamePlaylist(playlist: Playlist, name: String) { override suspend fun renamePlaylist(playlist: Playlist, name: String) {
val userLibrary = synchronized(this) { userLibrary ?: return } val userLibrary = synchronized(this) { userLibrary ?: return }
logD("Renaming $playlist to $name") logD("Renaming $playlist to $name")
userLibrary.renamePlaylist(playlist, name) userLibrary.renamePlaylist(playlist, name)
emitLibraryChange(device = false, user = true) dispatchLibraryChange(device = false, user = true)
} }
override suspend fun deletePlaylist(playlist: Playlist) { override suspend fun deletePlaylist(playlist: Playlist) {
val userLibrary = synchronized(this) { userLibrary ?: return } val userLibrary = synchronized(this) { userLibrary ?: return }
logD("Deleting $playlist") logD("Deleting $playlist")
userLibrary.deletePlaylist(playlist) userLibrary.deletePlaylist(playlist)
emitLibraryChange(device = false, user = true) dispatchLibraryChange(device = false, user = true)
} }
override suspend fun addToPlaylist(songs: List<Song>, playlist: Playlist) { override suspend fun addToPlaylist(songs: List<Song>, playlist: Playlist) {
val userLibrary = synchronized(this) { userLibrary ?: return } val userLibrary = synchronized(this) { userLibrary ?: return }
logD("Adding ${songs.size} songs to $playlist") logD("Adding ${songs.size} songs to $playlist")
userLibrary.addToPlaylist(playlist, songs) userLibrary.addToPlaylist(playlist, songs)
emitLibraryChange(device = false, user = true) dispatchLibraryChange(device = false, user = true)
} }
override suspend fun rewritePlaylist(playlist: Playlist, songs: List<Song>) { override suspend fun rewritePlaylist(playlist: Playlist, songs: List<Song>) {
val userLibrary = synchronized(this) { userLibrary ?: return } val userLibrary = synchronized(this) { userLibrary ?: return }
logD("Rewriting $playlist with ${songs.size} songs") logD("Rewriting $playlist with ${songs.size} songs")
userLibrary.rewritePlaylist(playlist, songs) userLibrary.rewritePlaylist(playlist, songs)
emitLibraryChange(device = false, user = true) dispatchLibraryChange(device = false, user = true)
} }
@Synchronized @Synchronized
@ -353,7 +354,7 @@ constructor(
// Music loading process failed due to something we have not handled. // Music loading process failed due to something we have not handled.
logE("Music indexing failed") logE("Music indexing failed")
logE(e.stackTraceToString()) logE(e.stackTraceToString())
emitComplete(e) emitIndexingCompletion(e)
} }
} }
@ -367,7 +368,7 @@ constructor(
// Start initializing the extractors. Use an indeterminate state, as there is no ETA on // Start initializing the extractors. Use an indeterminate state, as there is no ETA on
// how long a media database query will take. // how long a media database query will take.
emitLoading(IndexingProgress.Indeterminate) emitIndexingProgress(IndexingProgress.Indeterminate)
// Do the initial query of the cache and media databases in parallel. // Do the initial query of the cache and media databases in parallel.
logD("Starting MediaStore query") logD("Starting MediaStore query")
@ -388,6 +389,7 @@ constructor(
logD("Starting song discovery") logD("Starting song discovery")
val completeSongs = Channel<RawSong>(Channel.UNLIMITED) val completeSongs = Channel<RawSong>(Channel.UNLIMITED)
val incompleteSongs = Channel<RawSong>(Channel.UNLIMITED) val incompleteSongs = Channel<RawSong>(Channel.UNLIMITED)
val processedSongs = Channel<RawSong>(Channel.UNLIMITED)
logD("Started MediaStore discovery") logD("Started MediaStore discovery")
val mediaStoreJob = val mediaStoreJob =
worker.scope.tryAsync { worker.scope.tryAsync {
@ -400,12 +402,19 @@ constructor(
tagExtractor.consume(incompleteSongs, completeSongs) tagExtractor.consume(incompleteSongs, completeSongs)
completeSongs.close() completeSongs.close()
} }
logD("Starting DeviceLibrary creation")
val deviceLibraryJob =
worker.scope.tryAsync(Dispatchers.Default) {
deviceLibraryFactory.create(completeSongs, processedSongs).also {
processedSongs.close()
}
}
// Await completed raw songs as they are processed. // Await completed raw songs as they are processed.
val rawSongs = LinkedList<RawSong>() val rawSongs = LinkedList<RawSong>()
for (rawSong in completeSongs) { for (rawSong in processedSongs) {
rawSongs.add(rawSong) rawSongs.add(rawSong)
emitLoading(IndexingProgress.Songs(rawSongs.size, query.projectedTotal)) emitIndexingProgress(IndexingProgress.Songs(rawSongs.size, query.projectedTotal))
} }
logD("Awaiting discovery completion") logD("Awaiting discovery completion")
// These should be no-ops, but we need the error state to see if we should keep going. // These should be no-ops, but we need the error state to see if we should keep going.
@ -417,47 +426,45 @@ constructor(
throw NoMusicException() throw NoMusicException()
} }
// Successfully loaded the library, now save the cache, create the library, and // Successfully loaded the library, now save the cache and read playlist information
// read playlist information in parallel. // in parallel.
logD("Discovered ${rawSongs.size} songs, starting finalization") logD("Discovered ${rawSongs.size} songs, starting finalization")
// TODO: Indicate playlist state in loading process? emitIndexingProgress(IndexingProgress.Indeterminate)
emitLoading(IndexingProgress.Indeterminate) logD("Starting UserLibrary query")
val deviceLibraryChannel = Channel<DeviceLibrary>() val userLibraryQueryJob = worker.scope.tryAsync { userLibraryFactory.query() }
logD("Starting DeviceLibrary creation")
val deviceLibraryJob =
worker.scope.tryAsync(Dispatchers.Default) {
deviceLibraryFactory.create(rawSongs).also { deviceLibraryChannel.send(it) }
}
logD("Starting UserLibrary creation")
val userLibraryJob =
worker.scope.tryAsync {
userLibraryFactory.read(deviceLibraryChannel).also { deviceLibraryChannel.close() }
}
if (cache == null || cache.invalidated) { if (cache == null || cache.invalidated) {
logD("Writing cache [why=${cache?.invalidated}]") logD("Writing cache [why=${cache?.invalidated}]")
cacheRepository.writeCache(rawSongs) cacheRepository.writeCache(rawSongs)
} }
logD("Awaiting library creation") logD("Awaiting UserLibrary query")
val rawPlaylists = userLibraryQueryJob.await().getOrThrow()
logD("Awaiting DeviceLibrary creation")
val deviceLibrary = deviceLibraryJob.await().getOrThrow() val deviceLibrary = deviceLibraryJob.await().getOrThrow()
val userLibrary = userLibraryJob.await().getOrThrow() logD("Starting UserLibrary creation")
val userLibrary = userLibraryFactory.create(rawPlaylists, deviceLibrary)
logD("Successfully indexed music library [device=$deviceLibrary user=$userLibrary]") logD("Successfully indexed music library [device=$deviceLibrary user=$userLibrary]")
emitComplete(null) emitIndexingCompletion(null)
// Comparing the library instances is obscenely expensive, do it within the library // Comparing the library instances is obscenely expensive, do it within the library
val deviceLibraryChanged = this.deviceLibrary != deviceLibrary
val userLibraryChanged = this.userLibrary != userLibrary
if (!deviceLibraryChanged && !userLibraryChanged) {
logD("Library has not changed, skipping update")
return
}
val deviceLibraryChanged: Boolean
val userLibraryChanged: Boolean
synchronized(this) { synchronized(this) {
deviceLibraryChanged = this.deviceLibrary != deviceLibrary
userLibraryChanged = this.userLibrary != userLibrary
if (!deviceLibraryChanged && !userLibraryChanged) {
logD("Library has not changed, skipping update")
return
}
this.deviceLibrary = deviceLibrary this.deviceLibrary = deviceLibrary
this.userLibrary = userLibrary this.userLibrary = userLibrary
} }
emitLibraryChange(deviceLibraryChanged, userLibraryChanged) withContext(Dispatchers.Main) {
dispatchLibraryChange(deviceLibraryChanged, userLibraryChanged)
}
} }
/** /**
@ -476,7 +483,7 @@ constructor(
} }
} }
private suspend fun emitLoading(progress: IndexingProgress) { private suspend fun emitIndexingProgress(progress: IndexingProgress) {
yield() yield()
synchronized(this) { synchronized(this) {
currentIndexingState = IndexingState.Indexing(progress) currentIndexingState = IndexingState.Indexing(progress)
@ -486,7 +493,7 @@ constructor(
} }
} }
private suspend fun emitComplete(error: Exception?) { private suspend fun emitIndexingCompletion(error: Exception?) {
yield() yield()
synchronized(this) { synchronized(this) {
previousCompletedState = IndexingState.Completed(error) previousCompletedState = IndexingState.Completed(error)
@ -499,7 +506,7 @@ constructor(
} }
@Synchronized @Synchronized
private fun emitLibraryChange(device: Boolean, user: Boolean) { private fun dispatchLibraryChange(device: Boolean, user: Boolean) {
val changes = MusicRepository.Changes(device, user) val changes = MusicRepository.Changes(device, user)
logD("Dispatching library change [changes=$changes]") logD("Dispatching library change [changes=$changes]")
for (listener in updateListeners) { for (listener in updateListeners) {

View file

@ -32,19 +32,19 @@ import org.oxycblt.auxio.music.info.Date
import org.oxycblt.auxio.music.metadata.correctWhitespace import org.oxycblt.auxio.music.metadata.correctWhitespace
import org.oxycblt.auxio.music.metadata.splitEscaped import org.oxycblt.auxio.music.metadata.splitEscaped
@Database(entities = [CachedSong::class], version = 27, exportSchema = false) @Database(entities = [CachedSong::class], version = 32, exportSchema = false)
abstract class CacheDatabase : RoomDatabase() { abstract class CacheDatabase : RoomDatabase() {
abstract fun cachedSongsDao(): CachedSongsDao abstract fun cachedSongsDao(): CachedSongsDao
} }
@Dao @Dao
interface CachedSongsDao { interface CachedSongsDao {
@Query("SELECT * FROM ${CachedSong.TABLE_NAME}") suspend fun readSongs(): List<CachedSong> @Query("SELECT * FROM CachedSong") suspend fun readSongs(): List<CachedSong>
@Query("DELETE FROM ${CachedSong.TABLE_NAME}") suspend fun nukeSongs() @Query("DELETE FROM CachedSong") suspend fun nukeSongs()
@Insert suspend fun insertSongs(songs: List<CachedSong>) @Insert suspend fun insertSongs(songs: List<CachedSong>)
} }
@Entity(tableName = CachedSong.TABLE_NAME) @Entity
@TypeConverters(CachedSong.Converters::class) @TypeConverters(CachedSong.Converters::class)
data class CachedSong( data class CachedSong(
/** /**
@ -60,6 +60,10 @@ data class CachedSong(
var size: Long? = null, var size: Long? = null,
/** @see RawSong */ /** @see RawSong */
var durationMs: Long, var durationMs: Long,
/** @see RawSong.replayGainTrackAdjustment */
val replayGainTrackAdjustment: Float?,
/** @see RawSong.replayGainAlbumAdjustment */
val replayGainAlbumAdjustment: Float?,
/** @see RawSong.musicBrainzId */ /** @see RawSong.musicBrainzId */
var musicBrainzId: String? = null, var musicBrainzId: String? = null,
/** @see RawSong.name */ /** @see RawSong.name */
@ -97,7 +101,7 @@ data class CachedSong(
/** @see RawSong.genreNames */ /** @see RawSong.genreNames */
var genreNames: List<String> = listOf() var genreNames: List<String> = listOf()
) { ) {
fun copyToRaw(rawSong: RawSong): CachedSong { fun copyToRaw(rawSong: RawSong) {
rawSong.musicBrainzId = musicBrainzId rawSong.musicBrainzId = musicBrainzId
rawSong.name = name rawSong.name = name
rawSong.sortName = sortName rawSong.sortName = sortName
@ -105,6 +109,9 @@ data class CachedSong(
rawSong.size = size rawSong.size = size
rawSong.durationMs = durationMs rawSong.durationMs = durationMs
rawSong.replayGainTrackAdjustment = replayGainTrackAdjustment
rawSong.replayGainAlbumAdjustment = replayGainAlbumAdjustment
rawSong.track = track rawSong.track = track
rawSong.disc = disc rawSong.disc = disc
rawSong.subtitle = subtitle rawSong.subtitle = subtitle
@ -124,7 +131,6 @@ data class CachedSong(
rawSong.albumArtistSortNames = albumArtistSortNames rawSong.albumArtistSortNames = albumArtistSortNames
rawSong.genreNames = genreNames rawSong.genreNames = genreNames
return this
} }
object Converters { object Converters {
@ -141,8 +147,6 @@ data class CachedSong(
} }
companion object { companion object {
const val TABLE_NAME = "cached_songs"
fun fromRaw(rawSong: RawSong) = fun fromRaw(rawSong: RawSong) =
CachedSong( CachedSong(
mediaStoreId = mediaStoreId =
@ -155,6 +159,8 @@ data class CachedSong(
sortName = rawSong.sortName, sortName = rawSong.sortName,
size = rawSong.size, size = rawSong.size,
durationMs = requireNotNull(rawSong.durationMs) { "Invalid raw: No duration" }, durationMs = requireNotNull(rawSong.durationMs) { "Invalid raw: No duration" },
replayGainTrackAdjustment = rawSong.replayGainTrackAdjustment,
replayGainAlbumAdjustment = rawSong.replayGainAlbumAdjustment,
track = rawSong.track, track = rawSong.track,
disc = rawSong.disc, disc = rawSong.disc,
subtitle = rawSong.subtitle, subtitle = rawSong.subtitle,

View file

@ -43,8 +43,6 @@ class CacheRoomModule {
Room.databaseBuilder( Room.databaseBuilder(
context.applicationContext, CacheDatabase::class.java, "music_cache.db") context.applicationContext, CacheDatabase::class.java, "music_cache.db")
.fallbackToDestructiveMigration() .fallbackToDestructiveMigration()
.fallbackToDestructiveMigrationFrom(0)
.fallbackToDestructiveMigrationOnDowngrade()
.build() .build()
@Provides fun cachedSongsDao(database: CacheDatabase) = database.cachedSongsDao() @Provides fun cachedSongsDao(database: CacheDatabase) = database.cachedSongsDao()

View file

@ -22,7 +22,7 @@ import android.content.Context
import android.net.Uri import android.net.Uri
import android.provider.OpenableColumns import android.provider.OpenableColumns
import javax.inject.Inject import javax.inject.Inject
import org.oxycblt.auxio.list.Sort import kotlinx.coroutines.channels.Channel
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
@ -32,7 +32,8 @@ import org.oxycblt.auxio.music.MusicSettings
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.fs.contentResolverSafe import org.oxycblt.auxio.music.fs.contentResolverSafe
import org.oxycblt.auxio.music.fs.useQuery import org.oxycblt.auxio.music.fs.useQuery
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logW
import org.oxycblt.auxio.util.unlikelyToBeNull
/** /**
* Organized music library information obtained from device storage. * Organized music library information obtained from device storage.
@ -45,13 +46,13 @@ import org.oxycblt.auxio.util.logD
*/ */
interface DeviceLibrary { interface DeviceLibrary {
/** All [Song]s in this [DeviceLibrary]. */ /** All [Song]s in this [DeviceLibrary]. */
val songs: List<Song> val songs: Collection<Song>
/** All [Album]s in this [DeviceLibrary]. */ /** All [Album]s in this [DeviceLibrary]. */
val albums: List<Album> val albums: Collection<Album>
/** All [Artist]s in this [DeviceLibrary]. */ /** All [Artist]s in this [DeviceLibrary]. */
val artists: List<Artist> val artists: Collection<Artist>
/** All [Genre]s in this [DeviceLibrary]. */ /** All [Genre]s in this [DeviceLibrary]. */
val genres: List<Genre> val genres: Collection<Genre>
/** /**
* Find a [Song] instance corresponding to the given [Music.UID]. * Find a [Song] instance corresponding to the given [Music.UID].
@ -97,37 +98,166 @@ interface DeviceLibrary {
/** Constructs a [DeviceLibrary] implementation in an asynchronous manner. */ /** Constructs a [DeviceLibrary] implementation in an asynchronous manner. */
interface Factory { interface Factory {
/** /**
* Create a new [DeviceLibrary]. * Creates a new [DeviceLibrary] instance asynchronously based on the incoming stream of
* [RawSong] instances.
* *
* @param rawSongs [RawSong] instances to create a [DeviceLibrary] from. * @param rawSongs A stream of [RawSong] instances to process.
* @param processedSongs A stream of [RawSong] instances that will have been processed by
* the instance.
*/ */
suspend fun create(rawSongs: List<RawSong>): DeviceLibrary suspend fun create(
} rawSongs: Channel<RawSong>,
processedSongs: Channel<RawSong>
companion object { ): DeviceLibraryImpl
/**
* Create an instance of [DeviceLibrary].
*
* @param rawSongs [RawSong]s to create the library out of.
* @param settings [MusicSettings] required.
*/
fun from(rawSongs: List<RawSong>, settings: MusicSettings): DeviceLibrary =
DeviceLibraryImpl(rawSongs, settings)
} }
} }
class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: MusicSettings) : class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: MusicSettings) :
DeviceLibrary.Factory { DeviceLibrary.Factory {
override suspend fun create(rawSongs: List<RawSong>): DeviceLibrary = override suspend fun create(
DeviceLibraryImpl(rawSongs, musicSettings) rawSongs: Channel<RawSong>,
processedSongs: Channel<RawSong>
): DeviceLibraryImpl {
val songGrouping = mutableMapOf<Music.UID, SongImpl>()
val albumGrouping = mutableMapOf<RawAlbum.Key, Grouping<RawAlbum, SongImpl>>()
val artistGrouping = mutableMapOf<RawArtist.Key, Grouping<RawArtist, Music>>()
val genreGrouping = mutableMapOf<RawGenre.Key, Grouping<RawGenre, SongImpl>>()
// TODO: Use comparators here
// All music information is grouped as it is indexed by other components.
for (rawSong in rawSongs) {
val song = SongImpl(rawSong, musicSettings)
// At times the indexer produces duplicate songs, try to filter these. Comparing by
// UID is sufficient for something like this, and also prevents collisions from
// causing severe issues elsewhere.
if (songGrouping.containsKey(song.uid)) {
logW(
"Duplicate song found: ${song.path} " +
"collides with ${unlikelyToBeNull(songGrouping[song.uid]).path}")
// We still want to say that we "processed" the song so that the user doesn't
// get confused at why the bar was only partly filled by the end of the loading
// process.
processedSongs.send(rawSong)
continue
}
songGrouping[song.uid] = song
// Group the new song into an album.
val albumKey = song.rawAlbum.key
val albumBody = albumGrouping[albumKey]
if (albumBody != null) {
albumBody.music.add(song)
val prioritized = albumBody.raw.src
// Since albums are grouped fuzzily, we pick the song with the earliest track to
// use for album information to ensure consistent metadata and UIDs. Fall back to
// the name otherwise.
val higherPriority =
song.track != null &&
(prioritized.track == null ||
song.track < prioritized.track ||
(song.track == prioritized.track && song.name < prioritized.name))
if (higherPriority) {
albumBody.raw = PrioritizedRaw(song.rawAlbum, song)
}
} else {
// Need to initialize this grouping.
albumGrouping[albumKey] =
Grouping(PrioritizedRaw(song.rawAlbum, song), mutableSetOf(song))
}
// Group the song into each of it's artists.
for (rawArtist in song.rawArtists) {
val artistKey = rawArtist.key
val artistBody = artistGrouping[artistKey]
if (artistBody != null) {
// Since artists are not guaranteed to have songs, song artist information is
// de-prioritized compared to album artist information.
artistBody.music.add(song)
} else {
// Need to initialize this grouping.
artistGrouping[artistKey] =
Grouping(PrioritizedRaw(rawArtist, song), mutableSetOf(song))
}
}
// Group the song into each of it's genres.
for (rawGenre in song.rawGenres) {
val genreKey = rawGenre.key
val genreBody = genreGrouping[genreKey]
if (genreBody != null) {
genreBody.music.add(song)
// Genre information from higher songs in ascending alphabetical order are
// prioritized.
val prioritized = genreBody.raw.src
val higherPriority = song.name < prioritized.name
if (higherPriority) {
genreBody.raw = PrioritizedRaw(rawGenre, song)
}
} else {
// Need to initialize this grouping.
genreGrouping[genreKey] =
Grouping(PrioritizedRaw(rawGenre, song), mutableSetOf(song))
}
}
processedSongs.send(rawSong)
}
// 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) }
for (album in albums) {
for (rawArtist in album.rawArtists) {
val key = RawArtist.Key(rawArtist)
val body = artistGrouping[key]
if (body != null) {
body.music.add(album)
when (val prioritized = body.raw.src) {
// Immediately replace any songs that initially held the priority position.
is SongImpl -> body.raw = PrioritizedRaw(rawArtist, album)
is AlbumImpl -> {
// Album artist information from earlier dates is prioritized, as it is
// less likely to change with the addition of new tracks. Fall back to
// the name otherwise.
val prioritize =
album.dates != null &&
(prioritized.dates == null ||
album.dates < prioritized.dates ||
(album.dates == prioritized.dates &&
album.name < prioritized.name))
if (prioritize) {
body.raw = PrioritizedRaw(rawArtist, album)
}
}
else -> throw IllegalStateException()
}
} else {
// Need to initialize this grouping.
artistGrouping[key] =
Grouping(PrioritizedRaw(rawArtist, album), mutableSetOf(album))
}
}
}
// 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) }
return DeviceLibraryImpl(songGrouping.values.toSet(), albums, artists, genres)
}
} }
private class DeviceLibraryImpl(rawSongs: List<RawSong>, settings: MusicSettings) : DeviceLibrary { // TODO: Avoid redundant data creation
override val songs = buildSongs(rawSongs, settings)
override val albums = buildAlbums(songs, settings)
override val artists = buildArtists(songs, albums, settings)
override val genres = buildGenres(songs, settings)
class DeviceLibraryImpl(
override val songs: Set<SongImpl>,
override val albums: Set<AlbumImpl>,
override val artists: Set<ArtistImpl>,
override val genres: Set<GenreImpl>
) : DeviceLibrary {
// Use a mapping to make finding information based on it's UID much faster. // Use a mapping to make finding information based on it's UID much faster.
private val songUidMap = buildMap { songs.forEach { put(it.uid, it.finalize()) } } private val songUidMap = buildMap { songs.forEach { put(it.uid, it.finalize()) } }
private val albumUidMap = buildMap { albums.forEach { put(it.uid, it.finalize()) } } private val albumUidMap = buildMap { albums.forEach { put(it.uid, it.finalize()) } }
@ -138,12 +268,13 @@ private class DeviceLibraryImpl(rawSongs: List<RawSong>, settings: MusicSettings
override fun equals(other: Any?) = other is DeviceLibrary && other.songs == songs override fun equals(other: Any?) = other is DeviceLibrary && other.songs == songs
override fun hashCode() = songs.hashCode() override fun hashCode() = songs.hashCode()
override fun toString() = override fun toString() =
"DeviceLibrary(songs=${songs.size}, albums=${albums.size}, artists=${artists.size}, genres=${genres.size})" "DeviceLibrary(songs=${songs.size}, albums=${albums.size}, " +
"artists=${artists.size}, genres=${genres.size})"
override fun findSong(uid: Music.UID) = songUidMap[uid] override fun findSong(uid: Music.UID): Song? = songUidMap[uid]
override fun findAlbum(uid: Music.UID) = albumUidMap[uid] override fun findAlbum(uid: Music.UID): Album? = albumUidMap[uid]
override fun findArtist(uid: Music.UID) = artistUidMap[uid] override fun findArtist(uid: Music.UID): Artist? = artistUidMap[uid]
override fun findGenre(uid: Music.UID) = genreUidMap[uid] override fun findGenre(uid: Music.UID): Genre? = genreUidMap[uid]
override fun findSongForUri(context: Context, uri: Uri) = override fun findSongForUri(context: Context, uri: Uri) =
context.contentResolverSafe.useQuery( context.contentResolverSafe.useQuery(
@ -156,70 +287,4 @@ private class DeviceLibraryImpl(rawSongs: List<RawSong>, settings: MusicSettings
val size = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE)) val size = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE))
songs.find { it.path.name == displayName && it.size == size } songs.find { it.path.name == displayName && it.size == size }
} }
private fun buildSongs(rawSongs: List<RawSong>, settings: MusicSettings): List<SongImpl> {
val start = System.currentTimeMillis()
val songs =
Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING)
.songs(rawSongs.map { SongImpl(it, settings) }.distinctBy { it.uid })
logD("Successfully built ${songs.size} songs in ${System.currentTimeMillis() - start}ms")
return songs
}
private fun buildAlbums(songs: List<SongImpl>, settings: MusicSettings): List<AlbumImpl> {
val start = System.currentTimeMillis()
// Group songs by their singular raw album, then map the raw instances and their
// grouped songs to Album values. Album.Raw will handle the actual grouping rules.
val songsByAlbum = songs.groupBy { it.rawAlbum.key }
val albums = songsByAlbum.map { AlbumImpl(it.key.value, settings, it.value) }
logD("Successfully built ${albums.size} albums in ${System.currentTimeMillis() - start}ms")
return albums
}
private fun buildArtists(
songs: List<SongImpl>,
albums: List<AlbumImpl>,
settings: MusicSettings
): List<ArtistImpl> {
val start = System.currentTimeMillis()
// Add every raw artist credited to each Song/Album to the grouping. This way,
// different multi-artist combinations are not treated as different artists.
// Songs and albums are grouped by artist and album artist respectively.
val musicByArtist = mutableMapOf<RawArtist.Key, MutableList<Music>>()
for (song in songs) {
for (rawArtist in song.rawArtists) {
musicByArtist.getOrPut(rawArtist.key) { mutableListOf() }.add(song)
}
}
for (album in albums) {
for (rawArtist in album.rawArtists) {
musicByArtist.getOrPut(rawArtist.key) { mutableListOf() }.add(album)
}
}
// Convert the combined mapping into artist instances.
val artists = musicByArtist.map { ArtistImpl(it.key.value, settings, it.value) }
logD(
"Successfully built ${artists.size} artists in ${System.currentTimeMillis() - start}ms")
return artists
}
private fun buildGenres(songs: List<SongImpl>, settings: MusicSettings): List<GenreImpl> {
val start = System.currentTimeMillis()
// Add every raw genre credited to each Song to the grouping. This way,
// different multi-genre combinations are not treated as different genres.
val songsByGenre = mutableMapOf<RawGenre.Key, MutableList<SongImpl>>()
for (song in songs) {
for (rawGenre in song.rawGenres) {
songsByGenre.getOrPut(rawGenre.key) { mutableListOf() }.add(song)
}
}
// Convert the mapping into genre instances.
val genres = songsByGenre.map { GenreImpl(it.key.value, settings, it.value) }
logD("Successfully built ${genres.size} genres in ${System.currentTimeMillis() - start}ms")
return genres
}
} }

View file

@ -19,6 +19,7 @@
package org.oxycblt.auxio.music.device package org.oxycblt.auxio.music.device
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.image.extractor.CoverUri
import org.oxycblt.auxio.list.Sort import org.oxycblt.auxio.list.Sort
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
@ -37,6 +38,7 @@ import org.oxycblt.auxio.music.info.Name
import org.oxycblt.auxio.music.info.ReleaseType import org.oxycblt.auxio.music.info.ReleaseType
import org.oxycblt.auxio.music.metadata.parseId3GenreNames import org.oxycblt.auxio.music.metadata.parseId3GenreNames
import org.oxycblt.auxio.music.metadata.parseMultiValue import org.oxycblt.auxio.music.metadata.parseMultiValue
import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment
import org.oxycblt.auxio.util.nonZeroOrNull import org.oxycblt.auxio.util.nonZeroOrNull
import org.oxycblt.auxio.util.toUuidOrNull import org.oxycblt.auxio.util.toUuidOrNull
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
@ -88,6 +90,10 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son
fromFormat = null) fromFormat = null)
override val size = requireNotNull(rawSong.size) { "Invalid raw: No size" } override val size = requireNotNull(rawSong.size) { "Invalid raw: No size" }
override val durationMs = requireNotNull(rawSong.durationMs) { "Invalid raw: No duration" } override val durationMs = requireNotNull(rawSong.durationMs) { "Invalid raw: 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: No date added" }
private var _album: AlbumImpl? = null private var _album: AlbumImpl? = null
override val album: Album override val album: Album
@ -226,17 +232,16 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son
/** /**
* Library-backed implementation of [Album]. * Library-backed implementation of [Album].
* *
* @param rawAlbum The [RawAlbum] to derive the member data from. * @param grouping [Grouping] to derive the member data from.
* @param musicSettings [MusicSettings] to for user parsing configuration. * @param musicSettings [MusicSettings] to for user parsing configuration.
* @param songs The [Song]s that are a part of this [Album]. These items will be linked to this
* [Album].
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class AlbumImpl( class AlbumImpl(
private val rawAlbum: RawAlbum, grouping: Grouping<RawAlbum, SongImpl>,
musicSettings: MusicSettings, musicSettings: MusicSettings,
override val songs: List<SongImpl>
) : Album { ) : Album {
private val rawAlbum = grouping.raw.inner
override val uid = override val uid =
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID. // Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
rawAlbum.musicBrainzId?.let { Music.UID.musicBrainz(MusicMode.ALBUMS, it) } rawAlbum.musicBrainzId?.let { Music.UID.musicBrainz(MusicMode.ALBUMS, it) }
@ -250,7 +255,7 @@ class AlbumImpl(
override val name = Name.Known.from(rawAlbum.name, rawAlbum.sortName, musicSettings) override val name = Name.Known.from(rawAlbum.name, rawAlbum.sortName, musicSettings)
override val dates: Date.Range? override val dates: Date.Range?
override val releaseType = rawAlbum.releaseType ?: ReleaseType.Album(null) override val releaseType = rawAlbum.releaseType ?: ReleaseType.Album(null)
override val coverUri = rawAlbum.mediaStoreId.toCoverUri() override val coverUri = CoverUri(rawAlbum.mediaStoreId.toCoverUri(), grouping.raw.src.uri)
override val durationMs: Long override val durationMs: Long
override val dateAdded: Long override val dateAdded: Long
@ -258,6 +263,8 @@ class AlbumImpl(
override val artists: List<Artist> override val artists: List<Artist>
get() = _artists get() = _artists
override val songs: Set<Song> = grouping.music
private var hashCode = uid.hashCode() private var hashCode = uid.hashCode()
init { init {
@ -267,7 +274,7 @@ class AlbumImpl(
var earliestDateAdded: Long = Long.MAX_VALUE var earliestDateAdded: Long = Long.MAX_VALUE
// Do linking and value generation in the same loop for efficiency. // Do linking and value generation in the same loop for efficiency.
for (song in songs) { for (song in grouping.music) {
song.link(this) song.link(this)
if (song.date != null) { if (song.date != null) {
@ -342,18 +349,13 @@ class AlbumImpl(
/** /**
* Library-backed implementation of [Artist]. * Library-backed implementation of [Artist].
* *
* @param rawArtist The [RawArtist] to derive the member data from. * @param grouping [Grouping] to derive the member data from.
* @param musicSettings [MusicSettings] to for user parsing configuration. * @param musicSettings [MusicSettings] to for user parsing configuration.
* @param songAlbums A list of the [Song]s and [Album]s that are a part of this [Artist] , either
* through artist or album artist tags. Providing [Song]s to the artist is optional. These
* instances will be linked to this [Artist].
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class ArtistImpl( class ArtistImpl(grouping: Grouping<RawArtist, Music>, musicSettings: MusicSettings) : Artist {
private val rawArtist: RawArtist, private val rawArtist = grouping.raw.inner
musicSettings: MusicSettings,
songAlbums: List<Music>
) : Artist {
override val uid = override val uid =
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID. // Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
rawArtist.musicBrainzId?.let { Music.UID.musicBrainz(MusicMode.ARTISTS, it) } rawArtist.musicBrainzId?.let { Music.UID.musicBrainz(MusicMode.ARTISTS, it) }
@ -362,10 +364,10 @@ class ArtistImpl(
rawArtist.name?.let { Name.Known.from(it, rawArtist.sortName, musicSettings) } rawArtist.name?.let { Name.Known.from(it, rawArtist.sortName, musicSettings) }
?: Name.Unknown(R.string.def_artist) ?: Name.Unknown(R.string.def_artist)
override val songs: List<Song> override val songs: Set<Song>
override val albums: List<Album> override val albums: Set<Album>
override val explicitAlbums: List<Album> override val explicitAlbums: Set<Album>
override val implicitAlbums: List<Album> override val implicitAlbums: Set<Album>
override val durationMs: Long? override val durationMs: Long?
override lateinit var genres: List<Genre> override lateinit var genres: List<Genre>
@ -376,7 +378,7 @@ class ArtistImpl(
val distinctSongs = mutableSetOf<Song>() val distinctSongs = mutableSetOf<Song>()
val albumMap = mutableMapOf<Album, Boolean>() val albumMap = mutableMapOf<Album, Boolean>()
for (music in songAlbums) { for (music in grouping.music) {
when (music) { when (music) {
is SongImpl -> { is SongImpl -> {
music.link(this) music.link(this)
@ -393,10 +395,10 @@ class ArtistImpl(
} }
} }
songs = distinctSongs.toList() songs = distinctSongs
albums = Sort(Sort.Mode.ByDate, Sort.Direction.DESCENDING).albums(albumMap.keys) albums = albumMap.keys
explicitAlbums = albums.filter { unlikelyToBeNull(albumMap[it]) } explicitAlbums = albums.filterTo(mutableSetOf()) { albumMap[it] == true }
implicitAlbums = albums.filterNot { unlikelyToBeNull(albumMap[it]) } implicitAlbums = albums.filterNotTo(mutableSetOf()) { albumMap[it] == true }
durationMs = songs.sumOf { it.durationMs }.nonZeroOrNull() durationMs = songs.sumOf { it.durationMs }.nonZeroOrNull()
hashCode = 31 * hashCode + rawArtist.hashCode() hashCode = 31 * hashCode + rawArtist.hashCode()
@ -444,40 +446,38 @@ class ArtistImpl(
/** /**
* Library-backed implementation of [Genre]. * Library-backed implementation of [Genre].
* *
* @param rawGenre [RawGenre] to derive the member data from. * @param grouping [Grouping] to derive the member data from.
* @param musicSettings [MusicSettings] to for user parsing configuration. * @param musicSettings [MusicSettings] to for user parsing configuration.
* @param songs Child [SongImpl]s of this instance.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class GenreImpl( class GenreImpl(grouping: Grouping<RawGenre, SongImpl>, musicSettings: MusicSettings) : Genre {
private val rawGenre: RawGenre, private val rawGenre = grouping.raw.inner
musicSettings: MusicSettings,
override val songs: List<SongImpl>
) : Genre {
override val uid = Music.UID.auxio(MusicMode.GENRES) { update(rawGenre.name) } override val uid = Music.UID.auxio(MusicMode.GENRES) { update(rawGenre.name) }
override val name = override val name =
rawGenre.name?.let { Name.Known.from(it, rawGenre.name, musicSettings) } rawGenre.name?.let { Name.Known.from(it, rawGenre.name, musicSettings) }
?: Name.Unknown(R.string.def_genre) ?: Name.Unknown(R.string.def_genre)
override val artists: List<Artist> override val songs: Set<Song>
override val artists: Set<Artist>
override val durationMs: Long override val durationMs: Long
private var hashCode = uid.hashCode() private var hashCode = uid.hashCode()
init { init {
val distinctAlbums = mutableSetOf<Album>()
val distinctArtists = mutableSetOf<Artist>() val distinctArtists = mutableSetOf<Artist>()
var totalDuration = 0L var totalDuration = 0L
for (song in songs) { for (song in grouping.music) {
song.link(this) song.link(this)
distinctAlbums.add(song.album)
distinctArtists.addAll(song.artists) distinctArtists.addAll(song.artists)
totalDuration += song.durationMs totalDuration += song.durationMs
} }
artists = Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING).artists(distinctArtists) songs = grouping.music
artists = distinctArtists
durationMs = totalDuration durationMs = totalDuration
hashCode = 31 * hashCode + rawGenre.hashCode() hashCode = 31 * hashCode + rawGenre.hashCode()
hashCode = 31 * hashCode + songs.hashCode() hashCode = 31 * hashCode + songs.hashCode()
} }

View file

@ -51,6 +51,10 @@ data class RawSong(
var durationMs: Long? = null, var durationMs: Long? = null,
/** @see Song.mimeType */ /** @see Song.mimeType */
var extensionMimeType: String? = null, var extensionMimeType: String? = null,
/** @see Song.replayGainAdjustment */
var replayGainTrackAdjustment: Float? = null,
/** @see Song.replayGainAdjustment */
var replayGainAlbumAdjustment: Float? = null,
/** @see Music.UID */ /** @see Music.UID */
var musicBrainzId: String? = null, var musicBrainzId: String? = null,
/** @see Music.name */ /** @see Music.name */
@ -115,30 +119,33 @@ data class RawAlbum(
) { ) {
val key = Key(this) val key = Key(this)
/** Exposed information that denotes [RawAlbum] uniqueness. */ /**
data class Key(val value: RawAlbum) { * Allows [RawAlbum]s to be compared by "fundamental" information that is unlikely to change on
* an item-by-item
*/
data class Key(private val inner: RawAlbum) {
// Albums are grouped as follows: // Albums are grouped as follows:
// - If we have a MusicBrainz ID, only group by it. This allows different Albums with the // - If we have a MusicBrainz ID, only group by it. This allows different Albums with the
// same name to be differentiated, which is common in large libraries. // same name to be differentiated, which is common in large libraries.
// - If we do not have a MusicBrainz ID, compare by the lowercase album name and lowercase // - If we do not have a MusicBrainz ID, compare by the lowercase album name and lowercase
// artist name. This allows for case-insensitive artist/album grouping, which can be common // artist name. This allows for case-insensitive artist/album grouping, which can be common
// for albums/artists that have different naming (ex. "RAMMSTEIN" vs. "Rammstein"). // for albums/artists that have different naming (ex. "RAMMSTEIN" vs. "Rammstein").
private val artistKeys = inner.rawArtists.map { it.key }
// Cache the hash-code for HashMap efficiency. // Cache the hash-code for HashMap efficiency.
private val hashCode = private val hashCode =
value.musicBrainzId?.hashCode() inner.musicBrainzId?.hashCode()
?: (31 * value.name.lowercase().hashCode() + value.rawArtists.hashCode()) ?: (31 * inner.name.lowercase().hashCode() + artistKeys.hashCode())
override fun hashCode() = hashCode override fun hashCode() = hashCode
override fun equals(other: Any?) = override fun equals(other: Any?) =
other is Key && other is Key &&
when { when {
value.musicBrainzId != null && other.value.musicBrainzId != null -> inner.musicBrainzId != null && other.inner.musicBrainzId != null ->
value.musicBrainzId == other.value.musicBrainzId inner.musicBrainzId == other.inner.musicBrainzId
value.musicBrainzId == null && other.value.musicBrainzId == null -> inner.musicBrainzId == null && other.inner.musicBrainzId == null ->
other.value.name.equals(other.value.name, true) && inner.name.equals(other.inner.name, true) && artistKeys == other.artistKeys
other.value.rawArtists == other.value.rawArtists
else -> false else -> false
} }
} }
@ -164,7 +171,7 @@ data class RawArtist(
* Allows [RawArtist]s to be compared by "fundamental" information that is unlikely to change on * Allows [RawArtist]s to be compared by "fundamental" information that is unlikely to change on
* an item-by-item * an item-by-item
*/ */
data class Key(val value: RawArtist) { data class Key(private val inner: RawArtist) {
// Artists are grouped as follows: // Artists are grouped as follows:
// - If we have a MusicBrainz ID, only group by it. This allows different Artists with the // - If we have a MusicBrainz ID, only group by it. This allows different Artists with the
// same name to be differentiated, which is common in large libraries. // same name to be differentiated, which is common in large libraries.
@ -172,7 +179,7 @@ data class RawArtist(
// grouping to be case-insensitive. // grouping to be case-insensitive.
// Cache the hashCode for HashMap efficiency. // Cache the hashCode for HashMap efficiency.
private val hashCode = value.musicBrainzId?.hashCode() ?: value.name?.lowercase().hashCode() val hashCode = inner.musicBrainzId?.hashCode() ?: inner.name?.lowercase().hashCode()
// Compare names and MusicBrainz IDs in order to differentiate artists with the // Compare names and MusicBrainz IDs in order to differentiate artists with the
// same name in large libraries. // same name in large libraries.
@ -182,13 +189,13 @@ data class RawArtist(
override fun equals(other: Any?) = override fun equals(other: Any?) =
other is Key && other is Key &&
when { when {
value.musicBrainzId != null && other.value.musicBrainzId != null -> inner.musicBrainzId != null && other.inner.musicBrainzId != null ->
value.musicBrainzId == other.value.musicBrainzId inner.musicBrainzId == other.inner.musicBrainzId
value.musicBrainzId == null && other.value.musicBrainzId == null -> inner.musicBrainzId == null && other.inner.musicBrainzId == null ->
when { when {
value.name != null && other.value.name != null -> inner.name != null && other.inner.name != null ->
value.name.equals(other.value.name, true) inner.name.equals(other.inner.name, true)
value.name == null && other.value.name == null -> true inner.name == null && other.inner.name == null -> true
else -> false else -> false
} }
else -> false else -> false
@ -207,9 +214,13 @@ data class RawGenre(
) { ) {
val key = Key(this) val key = Key(this)
data class Key(val value: RawGenre) { /**
* Allows [RawGenre]s to be compared by "fundamental" information that is unlikely to change on
* an item-by-item
*/
data class Key(private val inner: RawGenre) {
// Cache the hashCode for HashMap efficiency. // Cache the hashCode for HashMap efficiency.
private val hashCode = value.name?.lowercase().hashCode() private val hashCode = inner.name?.lowercase().hashCode()
// Only group by the lowercase genre name. This allows Genre grouping to be // Only group by the lowercase genre name. This allows Genre grouping to be
// case-insensitive, which may be helpful in some libraries with different ways of // case-insensitive, which may be helpful in some libraries with different ways of
@ -219,10 +230,28 @@ data class RawGenre(
override fun equals(other: Any?) = override fun equals(other: Any?) =
other is Key && other is Key &&
when { when {
value.name != null && other.value.name != null -> inner.name != null && other.inner.name != null ->
value.name.equals(other.value.name, true) inner.name.equals(other.inner.name, true)
value.name == null && other.value.name == null -> true inner.name == null && other.inner.name == null -> true
else -> false else -> false
} }
} }
} }
/**
* Represents grouped music information and the prioritized raw information to eventually derive a
* [Music] implementation instance from.
*
* @param raw The current [PrioritizedRaw] that will be used for the finalized music information.
* @param music The child [Music] instances of the music information to be created.
*/
data class Grouping<R, M : Music>(var raw: PrioritizedRaw<R, M>, val music: MutableSet<M>)
/**
* Represents a [RawAlbum], [RawArtist], or [RawGenre] specifically chosen to create a [Music]
* instance from due to it being the most likely source of truth.
*
* @param inner The raw music instance that will be used.
* @param src The [Music] instance that the raw information was derived from.
*/
data class PrioritizedRaw<R, M : Music>(val inner: R, val src: M)

View file

@ -203,8 +203,8 @@ private data class IntelligentKnownName(override val raw: String, override val s
// Separate each token into their numeric and lexicographic counterparts. // Separate each token into their numeric and lexicographic counterparts.
if (token.first().isDigit()) { if (token.first().isDigit()) {
// The digit string comparison breaks with preceding zero digits, remove those // The digit string comparison breaks with preceding zero digits, remove those
// TODO: Handle zero digits in other languages val digits =
val digits = token.trimStart('0').ifEmpty { token } token.trimStart { Character.getNumericValue(it) == 0 }.ifEmpty { token }
// Other languages have other types of digit strings, still use collation keys // Other languages have other types of digit strings, still use collation keys
collationKey = COLLATOR.getCollationKey(digits) collationKey = COLLATOR.getCollationKey(digits)
type = SortToken.Type.NUMERIC type = SortToken.Type.NUMERIC

View file

@ -30,6 +30,7 @@ import org.oxycblt.auxio.music.fs.toAudioUri
import org.oxycblt.auxio.music.info.Date import org.oxycblt.auxio.music.info.Date
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.logW
import org.oxycblt.auxio.util.nonZeroOrNull
/** /**
* An processing abstraction over the [MetadataRetriever] and [TextTags] workflow that operates on * An processing abstraction over the [MetadataRetriever] and [TextTags] workflow that operates on
@ -140,15 +141,16 @@ private class TagWorkerImpl(
textFrames["TALB"]?.let { rawSong.albumName = it.first() } textFrames["TALB"]?.let { rawSong.albumName = it.first() }
textFrames["TSOA"]?.let { rawSong.albumSortName = it.first() } textFrames["TSOA"]?.let { rawSong.albumSortName = it.first() }
(textFrames["TXXX:musicbrainz album type"] (textFrames["TXXX:musicbrainz album type"]
?: textFrames["TXXX:releasetype"] ?: textFrames["GRP1"]) ?: textFrames["TXXX:releasetype"] ?:
// This is a non-standard iTunes extension
textFrames["GRP1"])
?.let { rawSong.releaseTypes = it } ?.let { rawSong.releaseTypes = it }
// Artist // Artist
textFrames["TXXX:musicbrainz artist id"]?.let { rawSong.artistMusicBrainzIds = it } textFrames["TXXX:musicbrainz artist id"]?.let { rawSong.artistMusicBrainzIds = it }
(textFrames["TXXX:artists"] ?: textFrames["TPE1"])?.let { rawSong.artistNames = it } (textFrames["TXXX:artists"] ?: textFrames["TPE1"])?.let { rawSong.artistNames = it }
(textFrames["TXXX:artists_sort"] ?: textFrames["TSOP"])?.let { (textFrames["TXXX:artistssort"] ?: textFrames["TXXX:artists_sort"] ?: textFrames["TSOP"])
rawSong.artistSortNames = it ?.let { rawSong.artistSortNames = it }
}
// Album artist // Album artist
textFrames["TXXX:musicbrainz album artist id"]?.let { textFrames["TXXX:musicbrainz album artist id"]?.let {
@ -157,16 +159,18 @@ private class TagWorkerImpl(
(textFrames["TXXX:albumartists"] ?: textFrames["TPE2"])?.let { (textFrames["TXXX:albumartists"] ?: textFrames["TPE2"])?.let {
rawSong.albumArtistNames = it rawSong.albumArtistNames = it
} }
(textFrames["TXXX:albumartists_sort"] ?: textFrames["TSO2"])?.let { (textFrames["TXXX:albumartistssort"]
rawSong.albumArtistSortNames = it ?: textFrames["TXXX:albumartists_sort"] ?: textFrames["TXXX:albumartistsort"]
} // This is a non-standard iTunes extension
?: textFrames["TSO2"])
?.let { rawSong.albumArtistSortNames = it }
// Genre // Genre
textFrames["TCON"]?.let { rawSong.genreNames = it } textFrames["TCON"]?.let { rawSong.genreNames = it }
// Compilation Flag // Compilation Flag
(textFrames["TCMP"] (textFrames["TCMP"] // This is a non-standard itunes extension
?: textFrames["TXXX:compilation"] ?: textFrames["TXXX:itunescompilation"]) ?: textFrames["TXXX:compilation"] ?: textFrames["TXXX:itunescompilation"])
?.let { ?.let {
// Ignore invalid instances of this tag // Ignore invalid instances of this tag
if (it.size != 1 || it[0] != "1") return@let if (it.size != 1 || it[0] != "1") return@let
@ -175,6 +179,14 @@ private class TagWorkerImpl(
rawSong.albumArtistNames.ifEmpty { COMPILATION_ALBUM_ARTISTS } rawSong.albumArtistNames.ifEmpty { COMPILATION_ALBUM_ARTISTS }
rawSong.releaseTypes = rawSong.releaseTypes.ifEmpty { COMPILATION_RELEASE_TYPES } rawSong.releaseTypes = rawSong.releaseTypes.ifEmpty { COMPILATION_RELEASE_TYPES }
} }
// ReplayGain information
textFrames["TXXX:replaygain_track_gain"]?.parseReplayGainAdjustment()?.let {
rawSong.replayGainTrackAdjustment = it
}
textFrames["TXXX:replaygain_album_gain"]?.parseReplayGainAdjustment()?.let {
rawSong.replayGainAlbumAdjustment = it
}
} }
private fun parseId3v23Date(textFrames: Map<String, List<String>>): Date? { private fun parseId3v23Date(textFrames: Map<String, List<String>>): Date? {
@ -249,14 +261,18 @@ private class TagWorkerImpl(
// Artist // Artist
comments["musicbrainz_artistid"]?.let { rawSong.artistMusicBrainzIds = it } comments["musicbrainz_artistid"]?.let { rawSong.artistMusicBrainzIds = it }
(comments["artists"] ?: comments["artist"])?.let { rawSong.artistNames = it } (comments["artists"] ?: comments["artist"])?.let { rawSong.artistNames = it }
(comments["artists_sort"] ?: comments["artistsort"])?.let { rawSong.artistSortNames = it } (comments["artistssort"] ?: comments["artists_sort"] ?: comments["artistsort"])?.let {
rawSong.artistSortNames = it
}
// Album artist // Album artist
comments["musicbrainz_albumartistid"]?.let { rawSong.albumArtistMusicBrainzIds = it } comments["musicbrainz_albumartistid"]?.let { rawSong.albumArtistMusicBrainzIds = it }
(comments["albumartists"] ?: comments["albumartist"])?.let { rawSong.albumArtistNames = it } (comments["albumartists"] ?: comments["album_artists"] ?: comments["albumartist"])?.let {
(comments["albumartists_sort"] ?: comments["albumartistsort"])?.let { rawSong.albumArtistNames = it
rawSong.albumArtistSortNames = it
} }
(comments["albumartistssort"]
?: comments["albumartists_sort"] ?: comments["albumartistsort"])
?.let { rawSong.albumArtistSortNames = it }
// Genre // Genre
comments["genre"]?.let { rawSong.genreNames = it } comments["genre"]?.let { rawSong.genreNames = it }
@ -270,10 +286,38 @@ private class TagWorkerImpl(
rawSong.albumArtistNames.ifEmpty { COMPILATION_ALBUM_ARTISTS } rawSong.albumArtistNames.ifEmpty { COMPILATION_ALBUM_ARTISTS }
rawSong.releaseTypes = rawSong.releaseTypes.ifEmpty { COMPILATION_RELEASE_TYPES } rawSong.releaseTypes = rawSong.releaseTypes.ifEmpty { COMPILATION_RELEASE_TYPES }
} }
// ReplayGain information
// Most ReplayGain tags are formatted as a simple decibel adjustment in a custom
// replaygain_*_gain tag, but opus has it's own "r128_*_gain" ReplayGain specification,
// which requires dividing the adjustment by 256 to get the gain. This is used alongside
// the base adjustment intrinsic to the format to create the normalized adjustment. This is
// normally the only tag used for opus files, but some software still writes replay gain
// tags anyway.
(comments["r128_track_gain"]?.parseReplayGainAdjustment()?.div(256)
?: comments["replaygain_track_gain"]?.parseReplayGainAdjustment())
?.let { rawSong.replayGainTrackAdjustment = it }
(comments["r128_album_gain"]?.parseReplayGainAdjustment()?.div(256)
?: comments["replaygain_album_gain"]?.parseReplayGainAdjustment())
?.let { rawSong.replayGainAlbumAdjustment = it }
} }
/**
* Parse a ReplayGain adjustment into a float value.
*
* @return A parsed adjustment float, or null if the adjustment had invalid formatting.
*/
private fun List<String>.parseReplayGainAdjustment() =
first().replace(REPLAYGAIN_ADJUSTMENT_FILTER_REGEX, "").toFloatOrNull()?.nonZeroOrNull()
private companion object { private companion object {
val COMPILATION_ALBUM_ARTISTS = listOf("Various Artists") val COMPILATION_ALBUM_ARTISTS = listOf("Various Artists")
val COMPILATION_RELEASE_TYPES = listOf("compilation") val COMPILATION_RELEASE_TYPES = listOf("compilation")
/**
* Matches non-float information from ReplayGain adjustments. Derived from vanilla music:
* https://github.com/vanilla-music/vanilla
*/
val REPLAYGAIN_ADJUSTMENT_FILTER_REGEX by lazy { Regex("[^\\d.-]") }
} }
} }

View file

@ -28,6 +28,8 @@ import androidx.media3.extractor.metadata.vorbis.VorbisComment
* *
* @param metadata The [Metadata] to wrap. * @param metadata The [Metadata] to wrap.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*
* TODO: Merge with TagWorker
*/ */
class TextTags(metadata: Metadata) { class TextTags(metadata: Metadata) {
private val _id3v2 = mutableMapOf<String, List<String>>() private val _id3v2 = mutableMapOf<String, List<String>>()

View file

@ -20,7 +20,6 @@ package org.oxycblt.auxio.music.user
import java.lang.Exception import java.lang.Exception
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.channels.Channel
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.music.MusicSettings
@ -38,10 +37,12 @@ import org.oxycblt.auxio.util.logE
* generally not expected to create this yourself, and instead rely on MusicRepository. * generally not expected to create this yourself, and instead rely on MusicRepository.
* *
* @author Alexander Capehart * @author Alexander Capehart
*
* TODO: Communicate errors
*/ */
interface UserLibrary { interface UserLibrary {
/** The current user-defined playlists. */ /** The current user-defined playlists. */
val playlists: List<Playlist> val playlists: Collection<Playlist>
/** /**
* Find a [Playlist] instance corresponding to the given [Music.UID]. * Find a [Playlist] instance corresponding to the given [Music.UID].
@ -62,14 +63,25 @@ interface UserLibrary {
/** Constructs a [UserLibrary] implementation in an asynchronous manner. */ /** Constructs a [UserLibrary] implementation in an asynchronous manner. */
interface Factory { interface Factory {
/** /**
* Create a new [UserLibrary]. * Read all [RawPlaylist] information from the database, which can be transformed into a
* [UserLibrary] later.
* *
* @param deviceLibraryChannel Asynchronously populated [DeviceLibrary] that can be obtained * @return A list of [RawPlaylist]s.
* later. This allows database information to be read before the actual instance is
* constructed.
* @return A new [MutableUserLibrary] with the required implementation.
*/ */
suspend fun read(deviceLibraryChannel: Channel<DeviceLibrary>): MutableUserLibrary suspend fun query(): List<RawPlaylist>
/**
* Create a new [UserLibrary] from read [RawPlaylist] instances and a precursor
* [DeviceLibrary].
*
* @param rawPlaylists The [RawPlaylist]s to use.
* @param deviceLibrary The [DeviceLibrary] to use.
* @return The new [UserLibrary] instance.
*/
suspend fun create(
rawPlaylists: List<RawPlaylist>,
deviceLibrary: DeviceLibrary
): MutableUserLibrary
} }
} }
@ -85,55 +97,63 @@ interface MutableUserLibrary : UserLibrary {
* *
* @param name The name of the [Playlist]. * @param name The name of the [Playlist].
* @param songs The songs to place in the [Playlist]. * @param songs The songs to place in the [Playlist].
* @return The new [Playlist] instance, or null if one could not be created.
*/ */
suspend fun createPlaylist(name: String, songs: List<Song>) suspend fun createPlaylist(name: String, songs: List<Song>): Playlist?
/** /**
* Rename a [Playlist]. * Rename a [Playlist].
* *
* @param playlist The [Playlist] to rename. * @param playlist The [Playlist] to rename.
* @param name The name of the new [Playlist]. * @param name The name of the new [Playlist].
* @return True if the [Playlist] was successfully renamed, false otherwise.
*/ */
suspend fun renamePlaylist(playlist: Playlist, name: String) suspend fun renamePlaylist(playlist: Playlist, name: String): Boolean
/** /**
* Delete a [Playlist]. * Delete a [Playlist].
* *
* @param playlist The playlist to delete. * @param playlist The playlist to delete.
* @return True if the [Playlist] was successfully deleted, false otherwise.
*/ */
suspend fun deletePlaylist(playlist: Playlist) suspend fun deletePlaylist(playlist: Playlist): Boolean
/** /**
* Add [Song]s to a [Playlist]. * Add [Song]s to a [Playlist].
* *
* @param playlist The [Playlist] to add to. Must currently exist. * @param playlist The [Playlist] to add to. Must currently exist.
* @param songs The [Song]s to add to the [Playlist].
* @return True if the [Song]s were successfully added, false otherwise.
*/ */
suspend fun addToPlaylist(playlist: Playlist, songs: List<Song>) suspend fun addToPlaylist(playlist: Playlist, songs: List<Song>): Boolean
/** /**
* Update the [Song]s of a [Playlist]. * Update the [Song]s of a [Playlist].
* *
* @param playlist The [Playlist] to update. * @param playlist The [Playlist] to update.
* @param songs The new [Song]s to be contained in the [Playlist]. * @param songs The new [Song]s to be contained in the [Playlist].
* @return True if the [Playlist] was successfully updated, false otherwise.
*/ */
suspend fun rewritePlaylist(playlist: Playlist, songs: List<Song>) suspend fun rewritePlaylist(playlist: Playlist, songs: List<Song>): Boolean
} }
class UserLibraryFactoryImpl class UserLibraryFactoryImpl
@Inject @Inject
constructor(private val playlistDao: PlaylistDao, private val musicSettings: MusicSettings) : constructor(private val playlistDao: PlaylistDao, private val musicSettings: MusicSettings) :
UserLibrary.Factory { UserLibrary.Factory {
override suspend fun read(deviceLibraryChannel: Channel<DeviceLibrary>): MutableUserLibrary { override suspend fun query() =
// While were waiting for the library, read our playlists out. try {
val rawPlaylists = playlistDao.readRawPlaylists()
try { } catch (e: Exception) {
playlistDao.readRawPlaylists() logE("Unable to read playlists: $e")
} catch (e: Exception) { listOf()
logE("Unable to read playlists: $e") }
return UserLibraryImpl(playlistDao, mutableMapOf(), musicSettings)
} override suspend fun create(
rawPlaylists: List<RawPlaylist>,
deviceLibrary: DeviceLibrary
): MutableUserLibrary {
logD("Successfully read ${rawPlaylists.size} playlists") logD("Successfully read ${rawPlaylists.size} playlists")
val deviceLibrary = deviceLibraryChannel.receive()
// Convert the database playlist information to actual usable playlists. // Convert the database playlist information to actual usable playlists.
val playlistMap = mutableMapOf<Music.UID, PlaylistImpl>() val playlistMap = mutableMapOf<Music.UID, PlaylistImpl>()
for (rawPlaylist in rawPlaylists) { for (rawPlaylist in rawPlaylists) {
@ -153,89 +173,106 @@ private class UserLibraryImpl(
override fun equals(other: Any?) = other is UserLibraryImpl && other.playlistMap == playlistMap override fun equals(other: Any?) = other is UserLibraryImpl && other.playlistMap == playlistMap
override fun toString() = "UserLibrary(playlists=${playlists.size})" override fun toString() = "UserLibrary(playlists=${playlists.size})"
override val playlists: List<Playlist> override val playlists: Collection<Playlist>
get() = playlistMap.values.toList() get() = playlistMap.values.toSet()
override fun findPlaylist(uid: Music.UID) = playlistMap[uid] override fun findPlaylist(uid: Music.UID) = playlistMap[uid]
override fun findPlaylist(name: String) = playlistMap.values.find { it.name.raw == name } override fun findPlaylist(name: String) = playlistMap.values.find { it.name.raw == name }
override suspend fun createPlaylist(name: String, songs: List<Song>) { override suspend fun createPlaylist(name: String, songs: List<Song>): Playlist? {
// TODO: Use synchronized with value access too
val playlistImpl = PlaylistImpl.from(name, songs, musicSettings) val playlistImpl = PlaylistImpl.from(name, songs, musicSettings)
synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl } synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl }
val rawPlaylist = val rawPlaylist =
RawPlaylist( RawPlaylist(
PlaylistInfo(playlistImpl.uid, playlistImpl.name.raw), PlaylistInfo(playlistImpl.uid, playlistImpl.name.raw),
playlistImpl.songs.map { PlaylistSong(it.uid) }) playlistImpl.songs.map { PlaylistSong(it.uid) })
try {
return try {
playlistDao.insertPlaylist(rawPlaylist) playlistDao.insertPlaylist(rawPlaylist)
logD("Successfully created playlist $name with ${songs.size} songs") logD("Successfully created playlist $name with ${songs.size} songs")
playlistImpl
} catch (e: Exception) { } catch (e: Exception) {
logE("Unable to create playlist $name with ${songs.size} songs") logE("Unable to create playlist $name with ${songs.size} songs")
logE(e.stackTraceToString()) logE(e.stackTraceToString())
synchronized(this) { playlistMap.remove(playlistImpl.uid) } synchronized(this) { playlistMap.remove(playlistImpl.uid) }
return null
} }
} }
override suspend fun renamePlaylist(playlist: Playlist, name: String) { override suspend fun renamePlaylist(playlist: Playlist, name: String): Boolean {
val playlistImpl = val playlistImpl =
requireNotNull(playlistMap[playlist.uid]) { "Cannot rename invalid playlist" } synchronized(this) {
synchronized(this) { playlistMap[playlist.uid] = playlistImpl.edit(name, musicSettings) } requireNotNull(playlistMap[playlist.uid]) { "Cannot rename invalid playlist" }
try { .also { playlistMap[it.uid] = it.edit(name, musicSettings) }
}
return try {
playlistDao.replacePlaylistInfo(PlaylistInfo(playlist.uid, name)) playlistDao.replacePlaylistInfo(PlaylistInfo(playlist.uid, name))
logD("Successfully renamed $playlist to $name") logD("Successfully renamed $playlist to $name")
true
} catch (e: Exception) { } catch (e: Exception) {
logE("Unable to rename $playlist to $name: $e") logE("Unable to rename $playlist to $name: $e")
logE(e.stackTraceToString()) logE(e.stackTraceToString())
synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl } synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl }
return false
} }
} }
override suspend fun deletePlaylist(playlist: Playlist) { override suspend fun deletePlaylist(playlist: Playlist): Boolean {
val playlistImpl = val playlistImpl =
requireNotNull(playlistMap[playlist.uid]) { "Cannot remove invalid playlist" } synchronized(this) {
synchronized(this) { playlistMap.remove(playlistImpl.uid) } requireNotNull(playlistMap[playlist.uid]) { "Cannot remove invalid playlist" }
try { .also { playlistMap.remove(it.uid) }
}
return try {
playlistDao.deletePlaylist(playlist.uid) playlistDao.deletePlaylist(playlist.uid)
logD("Successfully deleted $playlist") logD("Successfully deleted $playlist")
true
} catch (e: Exception) { } catch (e: Exception) {
logE("Unable to delete $playlist: $e") logE("Unable to delete $playlist: $e")
logE(e.stackTraceToString()) logE(e.stackTraceToString())
synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl } synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl }
return false
} }
} }
override suspend fun addToPlaylist(playlist: Playlist, songs: List<Song>) { override suspend fun addToPlaylist(playlist: Playlist, songs: List<Song>): Boolean {
val playlistImpl = val playlistImpl =
requireNotNull(playlistMap[playlist.uid]) { "Cannot add to invalid playlist" } synchronized(this) {
synchronized(this) { playlistMap[playlist.uid] = playlistImpl.edit { addAll(songs) } } requireNotNull(playlistMap[playlist.uid]) { "Cannot add to invalid playlist" }
try { .also { playlistMap[it.uid] = it.edit { addAll(songs) } }
}
return try {
playlistDao.insertPlaylistSongs(playlist.uid, songs.map { PlaylistSong(it.uid) }) playlistDao.insertPlaylistSongs(playlist.uid, songs.map { PlaylistSong(it.uid) })
logD("Successfully added ${songs.size} songs to $playlist") logD("Successfully added ${songs.size} songs to $playlist")
true
} catch (e: Exception) { } catch (e: Exception) {
logE("Unable to add ${songs.size} songs to $playlist: $e") logE("Unable to add ${songs.size} songs to $playlist: $e")
logE(e.stackTraceToString()) logE(e.stackTraceToString())
synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl } synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl }
return false
} }
} }
override suspend fun rewritePlaylist(playlist: Playlist, songs: List<Song>) { override suspend fun rewritePlaylist(playlist: Playlist, songs: List<Song>): Boolean {
val playlistImpl = val playlistImpl =
requireNotNull(playlistMap[playlist.uid]) { "Cannot rewrite invalid playlist" } synchronized(this) {
synchronized(this) { playlistMap[playlist.uid] = playlistImpl.edit(songs) } requireNotNull(playlistMap[playlist.uid]) { "Cannot rewrite invalid playlist" }
try { .also { playlistMap[it.uid] = it.edit(songs) }
}
return try {
playlistDao.replacePlaylistSongs(playlist.uid, songs.map { PlaylistSong(it.uid) }) playlistDao.replacePlaylistSongs(playlist.uid, songs.map { PlaylistSong(it.uid) })
logD("Successfully rewrote $playlist with ${songs.size} songs") logD("Successfully rewrote $playlist with ${songs.size} songs")
true
} catch (e: Exception) { } catch (e: Exception) {
logE("Unable to rewrite $playlist with ${songs.size} songs: $e") logE("Unable to rewrite $playlist with ${songs.size} songs: $e")
logE(e.stackTraceToString()) logE(e.stackTraceToString())
synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl } synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl }
return false
} }
} }
} }

View file

@ -43,7 +43,5 @@ class UserRoomModule {
Room.databaseBuilder( Room.databaseBuilder(
context.applicationContext, UserMusicDatabase::class.java, "user_music.db") context.applicationContext, UserMusicDatabase::class.java, "user_music.db")
.fallbackToDestructiveMigration() .fallbackToDestructiveMigration()
.fallbackToDestructiveMigrationFrom(0)
.fallbackToDestructiveMigrationOnDowngrade()
.build() .build()
} }

View file

@ -27,6 +27,7 @@ import androidx.room.PrimaryKey
import androidx.room.Query import androidx.room.Query
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.TypeConverters import androidx.room.TypeConverters
import androidx.room.migration.Migration
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.RepeatMode
@ -37,7 +38,7 @@ import org.oxycblt.auxio.playback.state.RepeatMode
*/ */
@Database( @Database(
entities = [PlaybackState::class, QueueHeapItem::class, QueueMappingItem::class], entities = [PlaybackState::class, QueueHeapItem::class, QueueMappingItem::class],
version = 27, version = 32,
exportSchema = false) exportSchema = false)
@TypeConverters(Music.UID.TypeConverters::class) @TypeConverters(Music.UID.TypeConverters::class)
abstract class PersistenceDatabase : RoomDatabase() { abstract class PersistenceDatabase : RoomDatabase() {
@ -54,6 +55,16 @@ abstract class PersistenceDatabase : RoomDatabase() {
* @return A [QueueDao] providing control of the database's queue tables. * @return A [QueueDao] providing control of the database's queue tables.
*/ */
abstract fun queueDao(): QueueDao abstract fun queueDao(): QueueDao
companion object {
val MIGRATION_27_32 =
Migration(27, 32) {
// Switched from custom names to just letting room pick the names
it.execSQL("ALTER TABLE playback_state RENAME TO PlaybackState")
it.execSQL("ALTER TABLE queue_heap RENAME TO QueueHeapItem")
it.execSQL("ALTER TABLE queue_mapping RENAME TO QueueMappingItem")
}
}
} }
/** /**
@ -68,11 +79,10 @@ interface PlaybackStateDao {
* *
* @return The previously persisted [PlaybackState], or null if one was not present. * @return The previously persisted [PlaybackState], or null if one was not present.
*/ */
@Query("SELECT * FROM ${PlaybackState.TABLE_NAME} WHERE id = 0") @Query("SELECT * FROM PlaybackState WHERE id = 0") suspend fun getState(): PlaybackState?
suspend fun getState(): PlaybackState?
/** Delete any previously persisted [PlaybackState]s. */ /** Delete any previously persisted [PlaybackState]s. */
@Query("DELETE FROM ${PlaybackState.TABLE_NAME}") suspend fun nukeState() @Query("DELETE FROM PlaybackState") suspend fun nukeState()
/** /**
* Insert a new [PlaybackState] into the database. * Insert a new [PlaybackState] into the database.
@ -94,21 +104,20 @@ interface QueueDao {
* *
* @return A list of persisted [QueueHeapItem]s wrapping each heap item. * @return A list of persisted [QueueHeapItem]s wrapping each heap item.
*/ */
@Query("SELECT * FROM ${QueueHeapItem.TABLE_NAME}") suspend fun getHeap(): List<QueueHeapItem> @Query("SELECT * FROM QueueHeapItem") suspend fun getHeap(): List<QueueHeapItem>
/** /**
* Get the previously persisted queue mapping. * Get the previously persisted queue mapping.
* *
* @return A list of persisted [QueueMappingItem]s wrapping each heap item. * @return A list of persisted [QueueMappingItem]s wrapping each heap item.
*/ */
@Query("SELECT * FROM ${QueueMappingItem.TABLE_NAME}") @Query("SELECT * FROM QueueMappingItem") suspend fun getMapping(): List<QueueMappingItem>
suspend fun getMapping(): List<QueueMappingItem>
/** Delete any previously persisted queue heap entries. */ /** Delete any previously persisted queue heap entries. */
@Query("DELETE FROM ${QueueHeapItem.TABLE_NAME}") suspend fun nukeHeap() @Query("DELETE FROM QueueHeapItem") suspend fun nukeHeap()
/** Delete any previously persisted queue mapping entries. */ /** Delete any previously persisted queue mapping entries. */
@Query("DELETE FROM ${QueueMappingItem.TABLE_NAME}") suspend fun nukeMapping() @Query("DELETE FROM QueueMappingItem") suspend fun nukeMapping()
/** /**
* Insert new heap entries into the database. * Insert new heap entries into the database.
@ -128,7 +137,7 @@ interface QueueDao {
// TODO: Figure out how to get RepeatMode to map to an int instead of a string // TODO: Figure out how to get RepeatMode to map to an int instead of a string
// TODO: Use intrinsic table names rather than custom names // TODO: Use intrinsic table names rather than custom names
@Entity(tableName = PlaybackState.TABLE_NAME) @Entity
data class PlaybackState( data class PlaybackState(
@PrimaryKey val id: Int, @PrimaryKey val id: Int,
val index: Int, val index: Int,
@ -136,26 +145,9 @@ data class PlaybackState(
val repeatMode: RepeatMode, val repeatMode: RepeatMode,
val songUid: Music.UID, val songUid: Music.UID,
val parentUid: Music.UID? val parentUid: Music.UID?
) { )
companion object {
const val TABLE_NAME = "playback_state"
}
}
@Entity(tableName = QueueHeapItem.TABLE_NAME) @Entity data class QueueHeapItem(@PrimaryKey val id: Int, val uid: Music.UID)
data class QueueHeapItem(@PrimaryKey val id: Int, val uid: Music.UID) {
companion object {
const val TABLE_NAME = "queue_heap"
}
}
@Entity(tableName = QueueMappingItem.TABLE_NAME) @Entity
data class QueueMappingItem( data class QueueMappingItem(@PrimaryKey val id: Int, val orderedIndex: Int, val shuffledIndex: Int)
@PrimaryKey val id: Int,
val orderedIndex: Int,
val shuffledIndex: Int
) {
companion object {
const val TABLE_NAME = "queue_mapping"
}
}

View file

@ -45,8 +45,7 @@ class PersistenceRoomModule {
PersistenceDatabase::class.java, PersistenceDatabase::class.java,
"playback_persistence.db") "playback_persistence.db")
.fallbackToDestructiveMigration() .fallbackToDestructiveMigration()
.fallbackToDestructiveMigrationFrom(1) .addMigrations(PersistenceDatabase.MIGRATION_27_32)
.fallbackToDestructiveMigrationOnDowngrade()
.build() .build()
@Provides fun playbackStateDao(database: PersistenceDatabase) = database.playbackStateDao() @Provides fun playbackStateDao(database: PersistenceDatabase) = database.playbackStateDao()

View file

@ -81,7 +81,7 @@ class PlayFromArtistDialog :
override fun onDestroyBinding(binding: DialogMusicChoicesBinding) { override fun onDestroyBinding(binding: DialogMusicChoicesBinding) {
super.onDestroyBinding(binding) super.onDestroyBinding(binding)
choiceAdapter binding.choiceRecycler.adapter = null
} }
override fun onClick(item: Artist, viewHolder: RecyclerView.ViewHolder) { override fun onClick(item: Artist, viewHolder: RecyclerView.ViewHolder) {

View file

@ -81,7 +81,7 @@ class PlayFromGenreDialog :
override fun onDestroyBinding(binding: DialogMusicChoicesBinding) { override fun onDestroyBinding(binding: DialogMusicChoicesBinding) {
super.onDestroyBinding(binding) super.onDestroyBinding(binding)
choiceAdapter binding.choiceRecycler.adapter = null
} }
override fun onClick(item: Genre, viewHolder: RecyclerView.ViewHolder) { override fun onClick(item: Genre, viewHolder: RecyclerView.ViewHolder) {

View file

@ -50,6 +50,15 @@ enum class ReplayGainMode {
} }
} }
/**
* Represents a ReplayGain adjustment to apply during song playback.
*
* @param track The track-specific adjustment that should be applied. Null if not available.
* @param album A more general album-specific adjustment that should be applied. Null if not
* available.
*/
data class ReplayGainAdjustment(val track: Float?, val album: Float?)
/** /**
* The current ReplayGain pre-amp configuration. * The current ReplayGain pre-amp configuration.
* *

View file

@ -21,15 +21,16 @@ package org.oxycblt.auxio.playback.replaygain
import androidx.media3.common.C import androidx.media3.common.C
import androidx.media3.common.Format import androidx.media3.common.Format
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.common.Tracks
import androidx.media3.common.audio.AudioProcessor import androidx.media3.common.audio.AudioProcessor
import androidx.media3.exoplayer.audio.BaseAudioProcessor import androidx.media3.exoplayer.audio.BaseAudioProcessor
import java.nio.ByteBuffer import java.nio.ByteBuffer
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.pow import kotlin.math.pow
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.metadata.TextTags import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackSettings import org.oxycblt.auxio.playback.PlaybackSettings
import org.oxycblt.auxio.playback.queue.Queue
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -48,9 +49,7 @@ class ReplayGainAudioProcessor
constructor( constructor(
private val playbackManager: PlaybackStateManager, private val playbackManager: PlaybackStateManager,
private val playbackSettings: PlaybackSettings private val playbackSettings: PlaybackSettings
) : BaseAudioProcessor(), Player.Listener, PlaybackSettings.Listener { ) : BaseAudioProcessor(), PlaybackStateManager.Listener, PlaybackSettings.Listener {
private var lastFormat: Format? = null
private var volume = 1f private var volume = 1f
set(value) { set(value) {
field = value field = value
@ -58,51 +57,38 @@ constructor(
flush() flush()
} }
/** init {
* Add this instance to the components required for it to function correctly. playbackManager.addListener(this)
*
* @param player The [Player] to attach to. Should already have this instance as an audio
* processor.
*/
fun addToListeners(player: Player) {
player.addListener(this)
playbackSettings.registerListener(this) playbackSettings.registerListener(this)
} }
/** /** Remove this instance from the components required for it to function correctly. */
* Remove this instance from the components required for it to function correctly. fun release() {
* playbackManager.removeListener(this)
* @param player The [Player] to detach from. Should already have this instance as an audio
* processor.
*/
fun releaseFromListeners(player: Player) {
player.removeListener(this)
playbackSettings.unregisterListener(this) playbackSettings.unregisterListener(this)
} }
// --- OVERRIDES --- // --- OVERRIDES ---
override fun onTracksChanged(tracks: Tracks) { override fun onIndexMoved(queue: Queue) {
super.onTracksChanged(tracks) logD("Index moved, updating current song")
// Try to find the currently playing track so we can update the ReplayGain adjustment applyReplayGain(queue.currentSong)
// based on it. }
for (group in tracks.groups) {
if (group.isSelected) { override fun onQueueChanged(queue: Queue, change: Queue.Change) {
for (i in 0 until group.length) { // Other types of queue changes preserve the current song.
if (group.isTrackSelected(i)) { if (change.type == Queue.Change.Type.SONG) {
applyReplayGain(group.getTrackFormat(i)) applyReplayGain(queue.currentSong)
return
}
}
}
} }
// Nothing selected, apply nothing }
applyReplayGain(null) override fun onNewPlayback(queue: Queue, parent: MusicParent?) {
logD("New playback started, updating playback information")
applyReplayGain(queue.currentSong)
} }
override fun onReplayGainSettingsChanged() { override fun onReplayGainSettingsChanged() {
// ReplayGain config changed, we need to set it up again. // ReplayGain config changed, we need to set it up again.
applyReplayGain(lastFormat) applyReplayGain(playbackManager.queue.currentSong)
} }
// --- REPLAYGAIN PARSING --- // --- REPLAYGAIN PARSING ---
@ -110,115 +96,63 @@ constructor(
/** /**
* Updates the volume adjustment based on the given [Format]. * Updates the volume adjustment based on the given [Format].
* *
* @param format The [Format] of the currently playing track, or null if nothing is playing. * @param song The [Format] of the currently playing track, or null if nothing is playing.
*/ */
private fun applyReplayGain(format: Format?) { private fun applyReplayGain(song: Song?) {
lastFormat = format if (song == null) {
val gain = parseReplayGain(format ?: return) logD("Nothing playing, disabling adjustment")
volume = 1f
return
}
logD("Applying ReplayGain adjustment for $song")
val gain = song.replayGainAdjustment
val preAmp = playbackSettings.replayGainPreAmp val preAmp = playbackSettings.replayGainPreAmp
val adjust = // ReplayGain is configurable, so determine what to do based off of the mode.
if (gain != null) { val resolvedAdjustment =
logD("Found ReplayGain adjustment $gain") when (playbackSettings.replayGainMode) {
// ReplayGain is configurable, so determine what to do based off of the mode. // User wants track gain to be preferred. Default to album gain only if
val useAlbumGain = // there is no track gain.
when (playbackSettings.replayGainMode) { ReplayGainMode.TRACK -> {
// User wants track gain to be preferred. Default to album gain only if logD("Using track strategy")
// there is no track gain. gain.track ?: gain.album
ReplayGainMode.TRACK -> { }
logD("Using track strategy") // User wants album gain to be preferred. Default to track gain only if
gain.track == 0f // here is no album gain.
} ReplayGainMode.ALBUM -> {
// User wants album gain to be preferred. Default to track gain only if logD("Using album strategy")
// here is no album gain. gain.album ?: gain.track
ReplayGainMode.ALBUM -> { }
logD("Using album strategy") // User wants album gain to be used when in an album, track gain otherwise.
gain.album != 0f ReplayGainMode.DYNAMIC -> {
} logD("Using dynamic strategy")
// User wants album gain to be used when in an album, track gain otherwise. gain.album?.takeIf {
ReplayGainMode.DYNAMIC -> { playbackManager.parent is Album &&
logD("Using dynamic strategy") playbackManager.queue.currentSong?.album == playbackManager.parent
playbackManager.parent is Album &&
playbackManager.queue.currentSong?.album == playbackManager.parent
}
} }
?: gain.track
}
}
val resolvedGain = val amplifiedAdjustment =
if (useAlbumGain) { if (resolvedAdjustment != null) {
logD("Using album gain") // Successfully resolved an adjustment, apply the corresponding pre-amp
gain.album logD("Applying with pre-amp")
} else { resolvedAdjustment + preAmp.with
logD("Using track gain")
gain.track
}
// Apply the adjustment specified when there is ReplayGain tags.
resolvedGain + preAmp.with
} else { } else {
// No ReplayGain tags existed, or no tags were parsable, or there was no metadata // No adjustment found, use the corresponding user-defined pre-amp
// in the first place. Return the gain to use when there is no ReplayGain value. logD("Applying without pre-amp")
logD("No ReplayGain tags present")
preAmp.without preAmp.without
} }
logD("Applying ReplayGain adjustment ${adjust}db") logD("Applying ReplayGain adjustment ${amplifiedAdjustment}db")
// Final adjustment along the volume curve. // Final adjustment along the volume curve.
volume = 10f.pow(adjust / 20f) volume = 10f.pow(amplifiedAdjustment / 20f)
} }
/**
* Parse ReplayGain information from the given [Format].
*
* @param format The [Format] to parse.
* @return A [Adjustment] adjustment, or null if there were no valid adjustments.
*/
private fun parseReplayGain(format: Format): Adjustment? {
val textTags = TextTags(format.metadata ?: return null)
var trackGain = 0f
var albumGain = 0f
// Most ReplayGain tags are formatted as a simple decibel adjustment in a custom
// replaygain_*_gain tag.
textTags.id3v2["TXXX:$TAG_RG_TRACK_GAIN"]
?.run { first().parseReplayGainAdjustment() }
?.let { trackGain = it }
textTags.id3v2["TXXX:$TAG_RG_ALBUM_GAIN"]
?.run { first().parseReplayGainAdjustment() }
?.let { albumGain = it }
textTags.vorbis[TAG_RG_ALBUM_GAIN]
?.run { first().parseReplayGainAdjustment() }
?.let { trackGain = it }
textTags.vorbis[TAG_RG_TRACK_GAIN]
?.run { first().parseReplayGainAdjustment() }
?.let { albumGain = it }
// Opus has it's own "r128_*_gain" ReplayGain specification, which requires dividing the
// adjustment by 256 to get the gain. This is used alongside the base adjustment
// intrinsic to the format to create the normalized adjustment. This is normally the only
// tag used for opus files, but some software still writes replay gain tags anyway.
textTags.vorbis[TAG_R128_TRACK_GAIN]
?.run { first().parseReplayGainAdjustment() }
?.let { trackGain = it / 256f }
textTags.vorbis[TAG_R128_ALBUM_GAIN]
?.run { first().parseReplayGainAdjustment() }
?.let { albumGain = it / 256f }
return if (trackGain != 0f || albumGain != 0f) {
Adjustment(trackGain, albumGain)
} else {
null
}
}
/**
* Parse a ReplayGain adjustment into a float value.
*
* @return A parsed adjustment float, or null if the adjustment had invalid formatting.
*/
private fun String.parseReplayGainAdjustment() =
replace(REPLAYGAIN_ADJUSTMENT_FILTER_REGEX, "").toFloatOrNull()
// --- AUDIO PROCESSOR IMPLEMENTATION --- // --- AUDIO PROCESSOR IMPLEMENTATION ---
override fun onConfigure( override fun onConfigure(
@ -284,25 +218,4 @@ constructor(
put(short.toByte()) put(short.toByte())
put(short.toInt().shr(8).toByte()) put(short.toInt().shr(8).toByte())
} }
/**
* The resolved ReplayGain adjustment for a file.
*
* @param track The track adjustment (in dB), or 0 if it is not present.
* @param album The album adjustment (in dB), or 0 if it is not present.
*/
private data class Adjustment(val track: Float, val album: Float)
private companion object {
const val TAG_RG_TRACK_GAIN = "replaygain_track_gain"
const val TAG_RG_ALBUM_GAIN = "replaygain_album_gain"
const val TAG_R128_TRACK_GAIN = "r128_track_gain"
const val TAG_R128_ALBUM_GAIN = "r128_album_gain"
/**
* Matches non-float information from ReplayGain adjustments. Derived from vanilla music:
* https://github.com/vanilla-music/vanilla
*/
val REPLAYGAIN_ADJUSTMENT_FILTER_REGEX by lazy { Regex("[^\\d.-]") }
}
} }

View file

@ -367,7 +367,7 @@ constructor(
.setSubtitle(song.artists.resolveNames(context)) .setSubtitle(song.artists.resolveNames(context))
// Since we usually have to load many songs into the queue, use the // Since we usually have to load many songs into the queue, use the
// MediaStore URI instead of loading a bitmap. // MediaStore URI instead of loading a bitmap.
.setIconUri(song.album.coverUri) .setIconUri(song.album.coverUri.mediaStore)
.setMediaUri(song.uri) .setMediaUri(song.uri)
.build() .build()
// Store the item index so we can then use the analogous index in the // Store the item index so we can then use the analogous index in the

View file

@ -144,7 +144,6 @@ class PlaybackService :
true) true)
.build() .build()
.also { it.addListener(this) } .also { it.addListener(this) }
replayGainProcessor.addToListeners(player)
foregroundManager = ForegroundManager(this) foregroundManager = ForegroundManager(this)
// Initialize any listener-dependent components last as we wouldn't want a listener race // Initialize any listener-dependent components last as we wouldn't want a listener race
// condition to cause us to load music before we were fully initialize. // condition to cause us to load music before we were fully initialize.
@ -196,7 +195,7 @@ class PlaybackService :
widgetComponent.release() widgetComponent.release()
mediaSessionComponent.release() mediaSessionComponent.release()
replayGainProcessor.releaseFromListeners(player) replayGainProcessor.release()
player.release() player.release()
if (openAudioEffectSession) { if (openAudioEffectSession) {
// Make sure to close the audio session when we release the player. // Make sure to close the audio session when we release the player.

View file

@ -58,11 +58,11 @@ interface SearchEngine {
* @param playlists A list of [Playlist], null if empty. * @param playlists A list of [Playlist], null if empty.
*/ */
data class Items( data class Items(
val songs: List<Song>?, val songs: Collection<Song>?,
val albums: List<Album>?, val albums: Collection<Album>?,
val artists: List<Artist>?, val artists: Collection<Artist>?,
val genres: List<Genre>?, val genres: Collection<Genre>?,
val playlists: List<Playlist>? val playlists: Collection<Playlist>?
) )
} }
@ -90,7 +90,7 @@ class SearchEngineImpl @Inject constructor(@ApplicationContext private val conte
* initially. This can be used to compare against additional attributes to improve search * initially. This can be used to compare against additional attributes to improve search
* result quality. * result quality.
*/ */
private inline fun <T : Music> List<T>.searchListImpl( private inline fun <T : Music> Collection<T>.searchListImpl(
query: String, query: String,
fallback: (String, T) -> Boolean = { _, _ -> false } fallback: (String, T) -> Boolean = { _, _ -> false }
) = ) =

View file

@ -286,7 +286,7 @@ fun Context.share(parent: MusicParent) = share(parent.songs)
* *
* @param songs The [Song]s to share. * @param songs The [Song]s to share.
*/ */
fun Context.share(songs: List<Song>) { fun Context.share(songs: Collection<Song>) {
if (songs.isEmpty()) return if (songs.isEmpty()) return
logD("Showing sharesheet for ${songs.size} songs") logD("Showing sharesheet for ${songs.size} songs")
val builder = ShareCompat.IntentBuilder(this) val builder = ShareCompat.IntentBuilder(this)

View file

@ -50,6 +50,13 @@ fun Int.nonZeroOrNull() = if (this > 0) this else null
*/ */
fun Long.nonZeroOrNull() = if (this > 0) this else null fun Long.nonZeroOrNull() = if (this > 0) this else null
/**
* Aliases a check to ensure that the given number is non-zero.
*
* @return The same number if it's non-zero, null otherwise.
*/
fun Float.nonZeroOrNull() = if (this > 0) this else null
/** /**
* Aliases a check to ensure a given value is in a specified range. * Aliases a check to ensure a given value is in a specified range.
* *

View file

@ -169,7 +169,7 @@
<string name="cdc_mka">Matroska-Audio</string> <string name="cdc_mka">Matroska-Audio</string>
<string name="cdc_aac">Advanced Audio Coding (AAC)</string> <string name="cdc_aac">Advanced Audio Coding (AAC)</string>
<string name="cdc_flac">Free Lossless Audio Codec (FLAC)</string> <string name="cdc_flac">Free Lossless Audio Codec (FLAC)</string>
<string name="fmt_bitrate">%d kbps</string> <string name="fmt_bitrate">%d kB/s</string>
<string name="fmt_sample_rate">%d Hz</string> <string name="fmt_sample_rate">%d Hz</string>
<string name="fmt_indexing">Lade deine Musikbibliothek… (%1$d/%2$d)</string> <string name="fmt_indexing">Lade deine Musikbibliothek… (%1$d/%2$d)</string>
<string name="lbl_shuffle_shortcut_short">Mischen</string> <string name="lbl_shuffle_shortcut_short">Mischen</string>

View file

@ -22,7 +22,7 @@
<string name="lbl_genre">Tyylilaji</string> <string name="lbl_genre">Tyylilaji</string>
<string name="lbl_genres">Tyylilajit</string> <string name="lbl_genres">Tyylilajit</string>
<string name="lbl_playlist">Soittolista</string> <string name="lbl_playlist">Soittolista</string>
<string name="lbl_mix">Mix</string> <string name="lbl_mix">DJ-mix</string>
<string name="lbl_live_group">Live</string> <string name="lbl_live_group">Live</string>
<string name="lbl_playlists">Soittolistat</string> <string name="lbl_playlists">Soittolistat</string>
<string name="lbl_search">Etsi</string> <string name="lbl_search">Etsi</string>
@ -164,7 +164,7 @@
<string name="lbl_album_remix">Remix-albumi</string> <string name="lbl_album_remix">Remix-albumi</string>
<string name="lbl_ep_remix">Remix-EP</string> <string name="lbl_ep_remix">Remix-EP</string>
<string name="lbl_single_remix">Remix-single</string> <string name="lbl_single_remix">Remix-single</string>
<string name="lbl_compilation_remix">Remix-kokoelmat</string> <string name="lbl_compilation_remix">Remix-kokoelma</string>
<string name="lng_indexing">Ladataan musiikkikirjastoa…</string> <string name="lng_indexing">Ladataan musiikkikirjastoa…</string>
<string name="lbl_version">Versio</string> <string name="lbl_version">Versio</string>
<string name="set_accent">Väriteema</string> <string name="set_accent">Väriteema</string>
@ -182,7 +182,7 @@
<string name="set_black_mode_desc">Käytä mustaa teemaa</string> <string name="set_black_mode_desc">Käytä mustaa teemaa</string>
<string name="set_round_mode">Pyöristetty tila</string> <string name="set_round_mode">Pyöristetty tila</string>
<string name="lbl_soundtracks">Elokuvamusiikit</string> <string name="lbl_soundtracks">Elokuvamusiikit</string>
<string name="lbl_mixes">Mixaukset</string> <string name="lbl_mixes">DJ-mixaukset</string>
<string name="err_no_perms">Auxio tarvitsee luvan lukea musiikkikirjastoa</string> <string name="err_no_perms">Auxio tarvitsee luvan lukea musiikkikirjastoa</string>
<string name="set_root_title">Asetukset</string> <string name="set_root_title">Asetukset</string>
<string name="lbl_sort">Järjestä</string> <string name="lbl_sort">Järjestä</string>
@ -243,4 +243,28 @@
<string name="set_restore_desc">Palauta aiemmin tallennettu toiston tila (jos olemassa)</string> <string name="set_restore_desc">Palauta aiemmin tallennettu toiston tila (jos olemassa)</string>
<string name="set_dirs_mode_exclude_desc">Musiikkia <b>ei</b> ladata valitsemistasi kansioista.</string> <string name="set_dirs_mode_exclude_desc">Musiikkia <b>ei</b> ladata valitsemistasi kansioista.</string>
<string name="set_replay_gain_mode_dynamic">Suosi albumia, jos sellaista toistetaan</string> <string name="set_replay_gain_mode_dynamic">Suosi albumia, jos sellaista toistetaan</string>
<string name="lbl_new_playlist">Uusi soittolista</string>
<string name="fmt_def_playlist">Soittolista %d</string>
<string name="lbl_playlist_add">Lisää soittolistaan</string>
<string name="def_song_count">Ei kappaleita</string>
<string name="set_round_mode_desc">Ota käyttöön pyöristetyt reunat käyttöliittymän lisäelementeissä (vaatii albumikansien olevan pyöristettyjä)</string>
<string name="set_observing_desc">Lataa musiikkikirjasto uudelleen sen muuttuessa (vaatii pysyvän ilmoituksen)</string>
<string name="set_separators_slash">Kauttaviiva (/)</string>
<string name="lbl_delete">Poista</string>
<string name="lbl_rename">Nimeä uudelleen</string>
<string name="lbl_rename_playlist">Nimeä soittolista uudelleen</string>
<string name="lbl_confirm_delete_playlist">Poistetaanko soittolista\?</string>
<string name="fmt_editing">Muokataan %s</string>
<string name="lbl_edit">Muokkaa</string>
<string name="desc_remove_song">Poista tämä kappale</string>
<string name="lng_playlist_created">Soittolista luotu</string>
<string name="lng_playlist_renamed">Soittolista nimetty uudelleen</string>
<string name="lng_playlist_deleted">Soittolista poistettu</string>
<string name="lng_playlist_added">Lisätty soittolistaan</string>
<string name="lbl_share">Jaa</string>
<string name="lbl_appears_on">Esiintyy</string>
<string name="lng_widget">Näytä ja hallinnoi musiikin toistoa</string>
<string name="desc_song_handle">Siirrä tämä kappale</string>
<string name="def_disc">Ei levyä</string>
<string name="fmt_deletion_info">Poistetaanko %s\? Tätä toimenpidettä ei voi perua.</string>
</resources> </resources>

View file

@ -41,7 +41,7 @@
<string name="err_no_music">Pas de musique trouvée</string> <string name="err_no_music">Pas de musique trouvée</string>
<!-- Description Namespace | Accessibility Strings --> <!-- Description Namespace | Accessibility Strings -->
<string name="desc_track_number">Morceau %d</string> <string name="desc_track_number">Morceau %d</string>
<string name="desc_play_pause">Lecture/Pause</string> <string name="desc_play_pause">Lecture ou pause</string>
<!-- Hint Namespace | EditText Hints --> <!-- Hint Namespace | EditText Hints -->
<string name="lng_search_library">Recherche dans votre bibliothèque…</string> <string name="lng_search_library">Recherche dans votre bibliothèque…</string>
<!-- Color Label namespace | Accent names --> <!-- Color Label namespace | Accent names -->
@ -50,26 +50,26 @@
<string name="clr_purple">Violet</string> <string name="clr_purple">Violet</string>
<string name="clr_indigo">Indigo</string> <string name="clr_indigo">Indigo</string>
<string name="clr_blue">Bleu</string> <string name="clr_blue">Bleu</string>
<string name="clr_deep_blue">Bleu Clair</string> <string name="clr_deep_blue">Bleu foncé</string>
<string name="clr_teal">Bleu Vert</string> <string name="clr_teal">Bleu Vert</string>
<string name="clr_green">Vert</string> <string name="clr_green">Vert</string>
<string name="clr_deep_green">Vert Clair</string> <string name="clr_deep_green">Vert foncé</string>
<string name="clr_lime">Vert Citron</string> <string name="clr_lime">Vert Citron</string>
<string name="clr_yellow">Jaune</string> <string name="clr_yellow">Jaune</string>
<string name="clr_orange">Orange</string> <string name="clr_orange">Orange</string>
<string name="clr_brown">Brun</string> <string name="clr_brown">Brun</string>
<string name="clr_grey">Gris</string> <string name="clr_grey">Gris</string>
<!-- Format Namespace | Value formatting/plurals --> <!-- Format Namespace | Value formatting/plurals -->
<string name="fmt_lib_song_count">Titres chargés: %d</string> <string name="fmt_lib_song_count">Titres chargés: %d</string>
<plurals name="fmt_song_count"> <plurals name="fmt_song_count">
<item quantity="one">%s Titre</item> <item quantity="one">%d titre</item>
<item quantity="many">%s Titres</item> <item quantity="many">%d titres</item>
<item quantity="other">%s Titres</item> <item quantity="other">%d titres</item>
</plurals> </plurals>
<plurals name="fmt_album_count"> <plurals name="fmt_album_count">
<item quantity="one">%s Album</item> <item quantity="one">%d album</item>
<item quantity="many">%s Albums</item> <item quantity="many">%d albums</item>
<item quantity="other">%s Albums</item> <item quantity="other">%d albums</item>
</plurals> </plurals>
<string name="lbl_format">Format</string> <string name="lbl_format">Format</string>
<string name="lbl_state_saved">État sauvegardé</string> <string name="lbl_state_saved">État sauvegardé</string>
@ -92,7 +92,7 @@
<string name="set_display">Affichage</string> <string name="set_display">Affichage</string>
<string name="set_lib_tabs">Onglets de la bibliothèque</string> <string name="set_lib_tabs">Onglets de la bibliothèque</string>
<string name="info_app_desc">Un lecteur de musique simple et rationnel pour Android.</string> <string name="info_app_desc">Un lecteur de musique simple et rationnel pour Android.</string>
<string name="lbl_indexer">Chargement de musique</string> <string name="lbl_indexer">Chargement de la musique</string>
<string name="lng_widget">Afficher et contrôler la lecture de la musique</string> <string name="lng_widget">Afficher et contrôler la lecture de la musique</string>
<string name="lng_indexing">Chargement de votre bibliothèque musicale…</string> <string name="lng_indexing">Chargement de votre bibliothèque musicale…</string>
<string name="lbl_name">Nom</string> <string name="lbl_name">Nom</string>
@ -106,7 +106,7 @@
<string name="lbl_song_detail">Voir les propriétés</string> <string name="lbl_song_detail">Voir les propriétés</string>
<string name="lbl_props">Propriétés de la chanson</string> <string name="lbl_props">Propriétés de la chanson</string>
<string name="lbl_ep_live">EP live</string> <string name="lbl_ep_live">EP live</string>
<string name="lbl_ep_remix">EP de remixes</string> <string name="lbl_ep_remix">EP de remix</string>
<string name="lbl_single_live">Single live</string> <string name="lbl_single_live">Single live</string>
<string name="lbl_single_remix">Single remixé</string> <string name="lbl_single_remix">Single remixé</string>
<string name="lbl_compilations">Compilations</string> <string name="lbl_compilations">Compilations</string>
@ -114,7 +114,7 @@
<string name="lbl_live_group">Live</string> <string name="lbl_live_group">Live</string>
<string name="lbl_indexing">Chargement de la musique</string> <string name="lbl_indexing">Chargement de la musique</string>
<string name="lbl_observing">Suivre la librairie musicale</string> <string name="lbl_observing">Suivre la librairie musicale</string>
<string name="lbl_eps">EPs</string> <string name="lbl_eps">EP</string>
<string name="lbl_ep">EP</string> <string name="lbl_ep">EP</string>
<string name="lbl_singles">Singles</string> <string name="lbl_singles">Singles</string>
<string name="lbl_single">Single</string> <string name="lbl_single">Single</string>
@ -125,7 +125,7 @@
<string name="lbl_remix_group">Remix</string> <string name="lbl_remix_group">Remix</string>
<string name="lbl_date_added">Date d\'ajout</string> <string name="lbl_date_added">Date d\'ajout</string>
<string name="lbl_album_live">Album live</string> <string name="lbl_album_live">Album live</string>
<string name="lbl_album_remix">Album de remixes</string> <string name="lbl_album_remix">Album de remix</string>
<string name="lbl_genre">Genre</string> <string name="lbl_genre">Genre</string>
<string name="lbl_equalizer">Égaliseur</string> <string name="lbl_equalizer">Égaliseur</string>
<string name="desc_shuffle_all">Lecture aléatoire de tous les titres</string> <string name="desc_shuffle_all">Lecture aléatoire de tous les titres</string>
@ -140,9 +140,9 @@
<string name="desc_music_dir_delete">Supprimer le dossier</string> <string name="desc_music_dir_delete">Supprimer le dossier</string>
<string name="def_artist">Artiste inconnu</string> <string name="def_artist">Artiste inconnu</string>
<string name="lbl_compilation_live">Compilation en direct</string> <string name="lbl_compilation_live">Compilation en direct</string>
<string name="lbl_compilation_remix">Compilations de remix</string> <string name="lbl_compilation_remix">Compilation de remix</string>
<string name="lbl_mixes">Mixes</string> <string name="lbl_mixes">Mix DJ</string>
<string name="lbl_mix">Mix</string> <string name="lbl_mix">Mix DJ</string>
<string name="err_bad_dir">Ce dossier n\'est pas pris en charge</string> <string name="err_bad_dir">Ce dossier n\'est pas pris en charge</string>
<string name="lbl_reset">Réinitialiser</string> <string name="lbl_reset">Réinitialiser</string>
<string name="cdc_ogg">Ogg audio</string> <string name="cdc_ogg">Ogg audio</string>
@ -194,7 +194,7 @@
<string name="set_separators_comma">Virgule (,)</string> <string name="set_separators_comma">Virgule (,)</string>
<string name="set_separators_semicolon">Point-virgule (;)</string> <string name="set_separators_semicolon">Point-virgule (;)</string>
<string name="set_exclude_non_music_desc">Ignorer les fichiers audio qui ne sont pas de la musique, tels que les podcasts</string> <string name="set_exclude_non_music_desc">Ignorer les fichiers audio qui ne sont pas de la musique, tels que les podcasts</string>
<string name="set_separators_warning">Avertissement: L\'utilisation de ce paramètre peut entraîner l\'interprétation incorrecte de certaines balises comme ayant plusieurs valeurs. Vous pouvez résoudre ce problème en préfixant les caractères de séparation indésirables avec une barre oblique inverse (\\).</string> <string name="set_separators_warning">Avertissement: L\'utilisation de ce paramètre peut entraîner l\'interprétation incorrecte de certaines balises comme ayant plusieurs valeurs. Vous pouvez résoudre ce problème en préfixant les caractères de séparation indésirables avec une barre oblique inverse (\\).</string>
<string name="set_exclude_non_music">Exclure non-musique</string> <string name="set_exclude_non_music">Exclure non-musique</string>
<string name="set_playback_mode_album">Lire depuis l\'album</string> <string name="set_playback_mode_album">Lire depuis l\'album</string>
<string name="set_separators_slash">Barre oblique (/)</string> <string name="set_separators_slash">Barre oblique (/)</string>
@ -236,4 +236,65 @@
<string name="set_reindex_desc">Recharger la bibliothèque musicale en utilisant si possible les étiquettes en cache</string> <string name="set_reindex_desc">Recharger la bibliothèque musicale en utilisant si possible les étiquettes en cache</string>
<string name="set_dirs_mode">Mode</string> <string name="set_dirs_mode">Mode</string>
<string name="set_dirs_mode_exclude">Exclure</string> <string name="set_dirs_mode_exclude">Exclure</string>
<string name="lbl_new_playlist">Nouvelle liste de lecture</string>
<string name="desc_skip_next">Passer à la chanson suivante</string>
<string name="desc_shuffle">Activer ou désactiver la lecture aléatoire</string>
<string name="fmt_sample_rate">%d Hz</string>
<string name="desc_skip_prev">Passer à la dernière chanson</string>
<string name="lbl_playlist_add">Ajouter à la liste de lecture</string>
<string name="desc_new_playlist">Créer une nouvelle liste de lecture</string>
<string name="cdc_mka">Audio Matroska</string>
<string name="fmt_lib_artist_count">Artistes chargés&amp;nbsp;: %d</string>
<string name="set_rewind_prev">Rembobiner avant de revenir en arrière</string>
<string name="desc_artist_image">Image d\'artiste pour %s</string>
<string name="def_track">Aucune piste</string>
<string name="def_playback">Aucune musique en cours de lecture</string>
<string name="lbl_delete">Supprimer</string>
<string name="set_repeat_pause_desc">Pause quand une chanson se répète</string>
<string name="desc_tab_handle">Déplacer cet onglet</string>
<string name="lbl_rename">Renommer</string>
<string name="err_did_not_wipe">Impossible d\'effacer l\'état</string>
<string name="desc_change_repeat">Modifier le mode de répétition</string>
<string name="fmt_lib_album_count">Albums chargés&amp;nbsp;: %d</string>
<string name="fmt_lib_total_duration">Durée totale&amp;nbsp;: %s</string>
<string name="desc_clear_search">Effacer la requête de recherche</string>
<string name="desc_playlist_image">Image de la liste de lecture pour %s</string>
<string name="fmt_disc_no">Disque %d</string>
<string name="fmt_indexing">Chargement de votre bibliothèque musicale… (%1$d/%2$d)</string>
<plurals name="fmt_artist_count">
<item quantity="one">%d artiste</item>
<item quantity="many">%d artistes</item>
<item quantity="other">%d artistes</item>
</plurals>
<string name="lbl_edit">Modifier</string>
<string name="set_repeat_pause">Pause en cas de répétition</string>
<string name="set_rewind_prev_desc">Rembobiner avant de passer à la chanson précédente</string>
<string name="desc_exit">Arrêter la lecture</string>
<string name="lng_playlist_created">Liste de lecture créée</string>
<string name="lng_playlist_renamed">Liste de lecture renommée</string>
<string name="lng_playlist_deleted">Liste de lecture supprimée</string>
<string name="lng_playlist_added">Ajouté à la liste de lecture</string>
<string name="fmt_bitrate">%d ko/s</string>
<string name="fmt_db_pos">+%.1f dB</string>
<string name="fmt_db_neg">-%.1f dB</string>
<string name="fmt_deletion_info">Supprimer %s&amp;nbsp;\? Cette opération ne peut pas être annulée.</string>
<string name="set_pre_amp_warning">Avertissement&amp;nbsp;: Le fait de régler le préamplificateur sur une valeur positive élevée peut entraîner l\'apparition des distortions sur certaines pistes audio.</string>
<string name="fmt_def_playlist">Liste de lecture %d</string>
<string name="lbl_appears_on">Apparaît sur</string>
<string name="lbl_rename_playlist">Renommer la liste de lecture</string>
<string name="lbl_confirm_delete_playlist">Supprimer la liste de lecture&amp;nbsp;\?</string>
<string name="lbl_share">Partager</string>
<string name="desc_remove_song">Retirer cette chanson</string>
<string name="desc_song_handle">Déplacer cette chanson</string>
<string name="desc_queue_bar">Ouvrir la file d\'attente</string>
<string name="err_did_not_save">Impossible de sauvegarder l\'état</string>
<string name="def_song_count">Aucune chanson</string>
<string name="fmt_editing">Modification de %s</string>
<string name="fmt_lib_genre_count">Genres chargés&amp;nbsp;: %d</string>
<string name="desc_genre_image">Image de genre pour %s</string>
<string name="cdc_flac">Codec audio gratuit sans perte (FLAC)</string>
<string name="fmt_selected">%d sélectionnés</string>
<string name="cdc_aac">Codage audio avancé (AAC)</string>
<string name="def_disc">Aucun disque</string>
<string name="fmt_list">%1$s, %2$s</string>
</resources> </resources>

View file

@ -59,4 +59,132 @@
<string name="lbl_indexer">गाने लोड हो रहे है</string> <string name="lbl_indexer">गाने लोड हो रहे है</string>
<string name="lbl_indexing">गाने लोड हो रहे है</string> <string name="lbl_indexing">गाने लोड हो रहे है</string>
<string name="info_app_desc">एंड्रॉयड के लिए एक सीधा साधा, विवेकशील गाने बजाने वाला ऐप।</string> <string name="info_app_desc">एंड्रॉयड के लिए एक सीधा साधा, विवेकशील गाने बजाने वाला ऐप।</string>
<string name="lbl_new_playlist">नई प्लेलिस्ट</string>
<string name="lbl_play_next">अगला चलाएं</string>
<string name="lbl_file_name">फ़ाइल का नाम</string>
<string name="set_lib_tabs">लायब्रेरी टैब्स</string>
<string name="set_playback_mode_album">एल्बम से चलाएं</string>
<string name="set_content">सामग्री</string>
<string name="fmt_selected">%d चयनित</string>
<string name="lbl_format">प्रारूप</string>
<string name="lbl_playlist_add">प्लेलिस्ट में जोड़ें</string>
<string name="lbl_relative_path">मुख्य पथ</string>
<string name="lbl_bitrate">बिट-रेट</string>
<string name="lbl_cancel">रद्द करें</string>
<string name="lbl_save">सहेजें</string>
<string name="set_ui_desc">एप्लिकेशन की थीम और रंग बदलें</string>
<string name="lng_playlist_added">प्लेलिस्ट में जोड़ा गया</string>
<string name="set_action_mode_next">अगले पर जाएं</string>
<string name="set_action_mode_repeat">रिपीट मोड</string>
<string name="set_content_desc">संगीत और छवियों को लोड करने के तरीके को नियंत्रित करें</string>
<string name="set_exclude_non_music">गैर-संगीत को बाहर रखें</string>
<string name="set_observing">स्वचालित पुनः लोडिंग</string>
<string name="fmt_db_pos">+%.1f dB</string>
<string name="fmt_def_playlist">प्लेलिस्ट %d</string>
<string name="fmt_list">%1$s, %2$s</string>
<string name="clr_dynamic">गतिशील</string>
<string name="set_ui">लुक और फील</string>
<string name="set_round_mode_desc">अतिरिक्त UI तत्वों पर गोल कोनों को सक्षम करें (एल्बम कवर को गोल करने की आवश्यकता है)</string>
<string name="set_playback_mode_none">दिखाए गए आइटम से चलाएँ</string>
<string name="set_library_song_playback_mode">लाइब्रेरी से चलाते समय</string>
<string name="set_observing_desc">संगीत लाइब्रेरी को फिर से लोड करें जब भी यह बदलता है (स्थाई नोटीफिकेशन की आवश्यकता होती है)</string>
<string name="set_exclude_non_music_desc">ऑडियो फ़ाइलों को अनदेखा करें जो संगीत नहीं हैं, जैसे कि पॉडकास्ट</string>
<string name="lbl_compilation_live">लाइव संकलन</string>
<string name="lbl_compilation_remix">रीमिक्स संकलन</string>
<string name="lbl_version">संस्करण</string>
<string name="lbl_shuffle_shortcut_long">सभी शफल करें</string>
<string name="lbl_playlist">प्लेलिस्ट</string>
<string name="lbl_playlists">प्लेलिस्टें</string>
<string name="set_round_mode">गोल मोड</string>
<string name="set_playback_mode_songs">सभी गीतों से चलाएं</string>
<string name="fmt_deletion_info">%s हटाएँ\? इसे पूर्ववत नहीं किया जा सकता।</string>
<string name="fmt_lib_song_count">लोड किए गए गाने: %d</string>
<string name="lbl_sort_dec">अवरोही</string>
<string name="lbl_play_selected">चयनित चलाएँ</string>
<string name="lbl_shuffle_selected">फेरबदल का चयन किया गया</string>
<string name="lbl_state_wiped">स्थिति साफ की गई</string>
<string name="lbl_state_saved">स्थिति सहेजी गई</string>
<string name="set_lib_tabs_desc">लायब्रेरी टैब की दृश्यता और क्रम बदलें</string>
<string name="set_music">संगीत</string>
<string name="set_personalize_desc">UI नियंत्रण और व्यवहार अनुकूलित करें</string>
<string name="fmt_lib_artist_count">कलाकार लोड किए गए: %d</string>
<string name="set_bar_action">कस्टम प्लेबैक बार एक्शन</string>
<string name="set_detail_song_playback_mode">आइटम विवरण से चलाते समय</string>
<string name="lbl_album_live">लाइव एल्बम</string>
<string name="lbl_album_remix">रीमिक्स एल्बम</string>
<string name="lbl_ep_live">लाइव EP</string>
<string name="lbl_ep_remix">रीमिक्स EP</string>
<string name="lbl_single_live">लाइव सिंगल</string>
<string name="lbl_single_remix">रीमिक्स सिंगल</string>
<string name="lbl_compilations">संकलन</string>
<string name="lbl_confirm_delete_playlist">प्लेलिस्ट हटाएँ\?</string>
<string name="set_black_mode">ब्लैक थीम</string>
<string name="lng_widget">संगीत प्लेबैक देखें और नियंत्रित करें</string>
<string name="lbl_reset">रीसेट</string>
<string name="lbl_wiki">विकी</string>
<string name="lbl_library_counts">लाइब्रेरी के आंकड़े</string>
<string name="fmt_lib_total_duration">कुल अवधि: %s</string>
<string name="lbl_equalizer">इक्वलाइज़र</string>
<string name="set_black_mode_desc">एक शुद्ध-काले डार्क थीम का उपयोग करें</string>
<string name="fmt_sample_rate">%d Hz</string>
<string name="lng_observing">परिवर्तनों के लिए आपकी संगीत लाइब्रेरी की निगरानी…</string>
<string name="lbl_eps">ईपी</string>
<string name="lbl_singles">एकल</string>
<string name="lbl_single">एकल</string>
<string name="lbl_album">एल्बम</string>
<string name="lbl_soundtrack">साउंडट्रैक</string>
<string name="lbl_soundtracks">साउंडट्रैकस</string>
<string name="lbl_mixtapes">मिश्रित टेपस</string>
<string name="lbl_mixtape">मिश्रित टेप</string>
<string name="lbl_remix_group">रीमिक्स</string>
<string name="lbl_live_group">रहना</string>
<string name="lbl_rename_playlist">प्लेलिस्ट का नाम बदलें</string>
<string name="lbl_delete">हटाएँ</string>
<string name="lbl_edit">संपादन करें</string>
<string name="set_behavior">व्यवहार</string>
<plurals name="fmt_artist_count">
<item quantity="one">%d कलाकार</item>
<item quantity="other">%d कलाकार</item>
</plurals>
<string name="lbl_observing">संगीत लाइब्रेरी की निगरानी</string>
<string name="lbl_add">जोड़ें</string>
<string name="lbl_ep">ईपी</string>
<string name="lbl_rename">नाम बदलें</string>
<string name="set_separators_semicolon">अर्धविराम (;)</string>
<string name="lbl_mixes">डीजे मिक्स</string>
<string name="lbl_mix">डीजे मिक्स</string>
<string name="lng_playlist_deleted">प्लेलिस्ट हटा दी गई</string>
<string name="lbl_date">दिनांक</string>
<string name="lbl_duration">अवधि</string>
<string name="lbl_song_count">गीतों की गिनती</string>
<string name="lbl_disc">डिस्क</string>
<string name="lbl_track">ट्रैक</string>
<string name="fmt_disc_no">डिस्क %d</string>
<string name="lbl_sample_rate">सैंपल रेट</string>
<string name="lbl_song_detail">गुणधर्म देखें</string>
<string name="lbl_props">गीत के गुणधर्म</string>
<string name="set_display">डिस्पले</string>
<string name="set_notif_action">कस्टम नोटीफिकेशन एक्शन</string>
<string name="set_playback_mode_artist">कलाकार से चलाएं</string>
<string name="set_playback_mode_genre">शैली से चलाएं</string>
<string name="set_keep_shuffle">फेरबदल याद रखें</string>
<string name="set_keep_shuffle_desc">नया गाना बजाते समय फेरबदल करते रहें</string>
<string name="set_separators">मल्टी-मूल्य विभाजक</string>
<string name="fmt_lib_genre_count">लोड की गई शैलियाँ: %d</string>
<string name="fmt_lib_album_count">लोड किए गए एल्बम: %d</string>
<string name="fmt_indexing">आपकी संगीत लाइब्रेरी लोड कर रहे हैं... (%1$d/%2$d)</string>
<string name="fmt_bitrate">%d kbps</string>
<string name="lng_indexing">आपकी संगीत लाइब्रेरी लोड कर रहे हैं…</string>
<string name="lng_playlist_created">प्लेलिस्ट बनाई गई</string>
<string name="lbl_appears_on">दिखाई देता है</string>
<string name="lbl_share">साझा करें</string>
<string name="lbl_shuffle_shortcut_short">शफल करें</string>
<string name="lbl_state_restored">स्थिति बहाल</string>
<string name="lng_playlist_renamed">प्लेलिस्ट का नाम बदला गया</string>
<string name="lng_author">अलेक्जेंडर कैपहार्ट द्वारा विकसित</string>
<string name="set_separators_desc">एकाधिक टैग मानों को निरूपित करने वाले वर्ण कॉन्फ़िगर करें</string>
<string name="set_separators_comma">अल्पविराम (,)</string>
<string name="set_separators_slash">स्लैश (/)</string>
<string name="fmt_db_neg">-%.1f dB</string>
<string name="fmt_editing">संपादन %s</string>
</resources> </resources>

View file

@ -149,10 +149,10 @@
<string name="fmt_bitrate">%d kbps</string> <string name="fmt_bitrate">%d kbps</string>
<string name="fmt_sample_rate">%d Hz</string> <string name="fmt_sample_rate">%d Hz</string>
<string name="fmt_indexing">Učitavanje tvoje zbirke blazbe … (%1$d/%2$d)</string> <string name="fmt_indexing">Učitavanje tvoje zbirke blazbe … (%1$d/%2$d)</string>
<string name="fmt_lib_song_count">Učitano pjesama: %d</string> <string name="fmt_lib_song_count">Broj učitanih pjesama: %d</string>
<string name="fmt_lib_album_count">Učitano albuma: %d</string> <string name="fmt_lib_album_count">Broj učitanih albuma: %d</string>
<string name="fmt_lib_artist_count">Učitanih izvođača: %d</string> <string name="fmt_lib_artist_count">Broj učitanih izvođača: %d</string>
<string name="fmt_lib_genre_count">Učitano žanrova: %d</string> <string name="fmt_lib_genre_count">Broj učitanih žanrova: %d</string>
<string name="fmt_lib_total_duration">Ukupno trajanje: %s</string> <string name="fmt_lib_total_duration">Ukupno trajanje: %s</string>
<plurals name="fmt_song_count"> <plurals name="fmt_song_count">
<item quantity="one">%d pjesma</item> <item quantity="one">%d pjesma</item>
@ -215,13 +215,13 @@
<string name="set_separators_and">Ampersand (&amp;)</string> <string name="set_separators_and">Ampersand (&amp;)</string>
<string name="lbl_compilation_live">Kompilacija uživo</string> <string name="lbl_compilation_live">Kompilacija uživo</string>
<string name="lbl_compilation_remix">Kompilacija remiksa</string> <string name="lbl_compilation_remix">Kompilacija remiksa</string>
<string name="lbl_mixes">Kompilacije</string> <string name="lbl_mixes">DJ kompilacije</string>
<string name="set_separators">Znakovi odjeljivanja vrijednosti</string> <string name="set_separators">Znakovi odjeljivanja vrijednosti</string>
<string name="desc_exit">Prekini reprodukciju</string> <string name="desc_exit">Prekini reprodukciju</string>
<string name="set_separators_desc">Konfiguriraj znakove koji označavaju višestruke vrijednosti oznaka</string> <string name="set_separators_desc">Konfiguriraj znakove koji označavaju višestruke vrijednosti oznaka</string>
<string name="set_separators_slash">Kosa crta (/)</string> <string name="set_separators_slash">Kosa crta (/)</string>
<string name="set_separators_plus">Plus (+)</string> <string name="set_separators_plus">Plus (+)</string>
<string name="lbl_mix">Kompilacija</string> <string name="lbl_mix">DJ kompilacija</string>
<string name="set_separators_semicolon">Točka-zarez (;)</string> <string name="set_separators_semicolon">Točka-zarez (;)</string>
<string name="set_bar_action">Prilagođena radnja trake reprodukcije</string> <string name="set_bar_action">Prilagođena radnja trake reprodukcije</string>
<string name="lbl_equalizer">Ekvilajzer</string> <string name="lbl_equalizer">Ekvilajzer</string>
@ -286,4 +286,8 @@
<string name="lng_playlist_added">Dodano u popis pjesama</string> <string name="lng_playlist_added">Dodano u popis pjesama</string>
<string name="lbl_edit">Uredi</string> <string name="lbl_edit">Uredi</string>
<string name="fmt_deletion_info">Izbrisati %s\? To je nepovratna radnja.</string> <string name="fmt_deletion_info">Izbrisati %s\? To je nepovratna radnja.</string>
<string name="fmt_editing">Uređivanje popisa pjesama %s</string>
<string name="lbl_appears_on">Sudjelovanja:</string>
<string name="lbl_share">Dijeli</string>
<string name="def_disc">Nema diska</string>
</resources> </resources>

View file

@ -8,7 +8,7 @@
<string name="lbl_grant">Permetti</string> <string name="lbl_grant">Permetti</string>
<string name="lbl_genres">Generi</string> <string name="lbl_genres">Generi</string>
<string name="lbl_artists">Artisti</string> <string name="lbl_artists">Artisti</string>
<string name="lbl_albums">Dischi</string> <string name="lbl_albums">Album</string>
<string name="lbl_songs">Canzoni</string> <string name="lbl_songs">Canzoni</string>
<string name="lbl_all_songs">Tutte le canzoni</string> <string name="lbl_all_songs">Tutte le canzoni</string>
<string name="lbl_search">Cerca</string> <string name="lbl_search">Cerca</string>
@ -17,21 +17,21 @@
<string name="lbl_sort">Ordine</string> <string name="lbl_sort">Ordine</string>
<string name="lbl_name">Nome</string> <string name="lbl_name">Nome</string>
<string name="lbl_artist">Artista</string> <string name="lbl_artist">Artista</string>
<string name="lbl_album">Disco</string> <string name="lbl_album">Album</string>
<string name="lbl_date">Anno</string> <string name="lbl_date">Anno</string>
<string name="lbl_sort_asc">Ascendente</string> <string name="lbl_sort_asc">Ascendente</string>
<string name="lbl_playback">Ora in riproduzione</string> <string name="lbl_playback">Ora in riproduzione</string>
<string name="lbl_play">Riproduci</string> <string name="lbl_play">Riproduci</string>
<string name="lbl_shuffle">Mescola</string> <string name="lbl_shuffle">Mescola</string>
<string name="set_playback_mode_songs">Riproduci da tutte le canzoni</string> <string name="set_playback_mode_songs">Riproduci da tutte le canzoni</string>
<string name="set_playback_mode_album">Riproduci dal disco</string> <string name="set_playback_mode_album">Riproduci dall\'album</string>
<string name="set_playback_mode_artist">Riproduci dall\'artista</string> <string name="set_playback_mode_artist">Riproduci dall\'artista</string>
<string name="lbl_queue">Coda</string> <string name="lbl_queue">Coda</string>
<string name="lbl_play_next">Riproduci successivo</string> <string name="lbl_play_next">Riproduci successivo</string>
<string name="lbl_queue_add">Accoda</string> <string name="lbl_queue_add">Accoda</string>
<string name="lng_queue_added">Accodato</string> <string name="lng_queue_added">Accodato</string>
<string name="lbl_go_artist">Vai all\'artista</string> <string name="lbl_go_artist">Vai all\'artista</string>
<string name="lbl_go_album">Vai al disco</string> <string name="lbl_go_album">Vai all\'album</string>
<string name="lbl_state_saved">Stato salvato</string> <string name="lbl_state_saved">Stato salvato</string>
<string name="lbl_add">Aggiungi</string> <string name="lbl_add">Aggiungi</string>
<string name="lbl_save">Salva</string> <string name="lbl_save">Salva</string>
@ -100,8 +100,8 @@
<string name="desc_clear_search">Cancella la query di ricerca</string> <string name="desc_clear_search">Cancella la query di ricerca</string>
<string name="desc_music_dir_delete">Rimuovi cartella</string> <string name="desc_music_dir_delete">Rimuovi cartella</string>
<string name="desc_auxio_icon">Icona Auxio</string> <string name="desc_auxio_icon">Icona Auxio</string>
<string name="desc_no_cover">Copertina disco</string> <string name="desc_no_cover">Copertina album</string>
<string name="desc_album_cover">Copertina disco per %s</string> <string name="desc_album_cover">Copertina album per %s</string>
<string name="desc_artist_image">Immagine artista per %s</string> <string name="desc_artist_image">Immagine artista per %s</string>
<string name="desc_genre_image">Immagine genere per %s</string> <string name="desc_genre_image">Immagine genere per %s</string>
<!-- Default Namespace | Placeholder values --> <!-- Default Namespace | Placeholder values -->
@ -129,7 +129,7 @@
<string name="clr_grey">Grigio</string> <string name="clr_grey">Grigio</string>
<!-- Format Namespace | Value formatting/plurals --> <!-- Format Namespace | Value formatting/plurals -->
<string name="fmt_lib_song_count">Canzoni trovate: %d</string> <string name="fmt_lib_song_count">Canzoni trovate: %d</string>
<string name="fmt_lib_album_count">Dischi trovati: %d</string> <string name="fmt_lib_album_count">Album trovati: %d</string>
<string name="fmt_lib_artist_count">Artisti trovati: %d</string> <string name="fmt_lib_artist_count">Artisti trovati: %d</string>
<string name="fmt_lib_genre_count">Generi trovati: %d</string> <string name="fmt_lib_genre_count">Generi trovati: %d</string>
<string name="fmt_lib_total_duration">Durata totale: %s</string> <string name="fmt_lib_total_duration">Durata totale: %s</string>
@ -139,9 +139,9 @@
<item quantity="other">%d canzoni</item> <item quantity="other">%d canzoni</item>
</plurals> </plurals>
<plurals name="fmt_album_count"> <plurals name="fmt_album_count">
<item quantity="one">%d disco</item> <item quantity="one">%d album</item>
<item quantity="many">%d dischi</item> <item quantity="many">%d album</item>
<item quantity="other">%d dischi</item> <item quantity="other">%d album</item>
</plurals> </plurals>
<string name="set_dirs_mode">Modo</string> <string name="set_dirs_mode">Modo</string>
<string name="set_dirs_mode_exclude_desc">La musica <b>non</b> sarà caricata dalle cartelle che aggiungi.</string> <string name="set_dirs_mode_exclude_desc">La musica <b>non</b> sarà caricata dalle cartelle che aggiungi.</string>
@ -169,7 +169,7 @@
<string name="cdc_flac">Free Lossless Audio Codec (FLAC)</string> <string name="cdc_flac">Free Lossless Audio Codec (FLAC)</string>
<string name="cdc_aac">Advanced Audio Coding (AAC)</string> <string name="cdc_aac">Advanced Audio Coding (AAC)</string>
<string name="fmt_disc_no">Disco %d</string> <string name="fmt_disc_no">Disco %d</string>
<string name="fmt_bitrate">%d kbps</string> <string name="fmt_bitrate">%d kB/s</string>
<string name="fmt_db_neg">-%.1f dB</string> <string name="fmt_db_neg">-%.1f dB</string>
<string name="lbl_indexer">Caricamento musica</string> <string name="lbl_indexer">Caricamento musica</string>
<string name="lng_indexing">Caricamento libreria musicale…</string> <string name="lng_indexing">Caricamento libreria musicale…</string>
@ -240,8 +240,8 @@
<string name="set_separators_and">E commerciale (&amp;)</string> <string name="set_separators_and">E commerciale (&amp;)</string>
<string name="lbl_compilation_live">Raccolte live</string> <string name="lbl_compilation_live">Raccolte live</string>
<string name="lbl_compilation_remix">Raccolta di remix</string> <string name="lbl_compilation_remix">Raccolta di remix</string>
<string name="lbl_mixes">Mixes</string> <string name="lbl_mixes">Mix DJ</string>
<string name="lbl_mix">Mix</string> <string name="lbl_mix">Mix DJ</string>
<string name="set_cover_mode_quality">Alta qualità</string> <string name="set_cover_mode_quality">Alta qualità</string>
<string name="set_separators_comma">Virgola (,)</string> <string name="set_separators_comma">Virgola (,)</string>
<string name="set_separators_semicolon">Punto e virgola (;)</string> <string name="set_separators_semicolon">Punto e virgola (;)</string>
@ -257,7 +257,7 @@
<string name="err_did_not_wipe">Impossibile svuotare</string> <string name="err_did_not_wipe">Impossibile svuotare</string>
<string name="lbl_shuffle_selected">Mescola selezionati</string> <string name="lbl_shuffle_selected">Mescola selezionati</string>
<string name="lbl_play_selected">Riproduci selezionati</string> <string name="lbl_play_selected">Riproduci selezionati</string>
<string name="fmt_selected">%d Selezionati</string> <string name="fmt_selected">%d selezionati</string>
<string name="set_playback_mode_genre">Riproduci dal genere</string> <string name="set_playback_mode_genre">Riproduci dal genere</string>
<string name="lbl_wiki">Wiki</string> <string name="lbl_wiki">Wiki</string>
<string name="fmt_list">%1$s, %2$s</string> <string name="fmt_list">%1$s, %2$s</string>
@ -295,4 +295,8 @@
<string name="fmt_deletion_info">Eliminare %s\? L\'operazione non può essere annullata.</string> <string name="fmt_deletion_info">Eliminare %s\? L\'operazione non può essere annullata.</string>
<string name="lng_playlist_deleted">Playlist eliminata</string> <string name="lng_playlist_deleted">Playlist eliminata</string>
<string name="lng_playlist_renamed">Playlist rinominata</string> <string name="lng_playlist_renamed">Playlist rinominata</string>
<string name="lbl_share">Condividi</string>
<string name="def_disc">Nessun disco</string>
<string name="lbl_appears_on">Appare su</string>
<string name="fmt_editing">Modifica di %s</string>
</resources> </resources>

View file

@ -9,7 +9,7 @@
<string name="lbl_artists">Artiesten</string> <string name="lbl_artists">Artiesten</string>
<string name="lbl_albums">Albums</string> <string name="lbl_albums">Albums</string>
<string name="lbl_songs">Nummers</string> <string name="lbl_songs">Nummers</string>
<string name="lbl_all_songs">Alle Nummers</string> <string name="lbl_all_songs">Alle nummers</string>
<string name="lbl_search">Zoeken</string> <string name="lbl_search">Zoeken</string>
<string name="lbl_filter">Filter</string> <string name="lbl_filter">Filter</string>
<string name="lbl_filter_all">Alles</string> <string name="lbl_filter_all">Alles</string>
@ -20,7 +20,7 @@
<string name="set_playback_mode_songs">Speel van alle nummers </string> <string name="set_playback_mode_songs">Speel van alle nummers </string>
<string name="set_playback_mode_album">Speel af van album </string> <string name="set_playback_mode_album">Speel af van album </string>
<string name="set_playback_mode_artist">Speel van artiest </string> <string name="set_playback_mode_artist">Speel van artiest </string>
<string name="lbl_playback">Afspeelscherm</string> <string name="lbl_playback">Nu afspelen</string>
<string name="lbl_queue">Wachtrij</string> <string name="lbl_queue">Wachtrij</string>
<string name="lbl_play_next">Afspelen als volgende</string> <string name="lbl_play_next">Afspelen als volgende</string>
<string name="lbl_queue_add">Toevoegen aan wachtrij</string> <string name="lbl_queue_add">Toevoegen aan wachtrij</string>
@ -30,15 +30,15 @@
<string name="lbl_state_saved">Staat gered</string> <string name="lbl_state_saved">Staat gered</string>
<string name="lbl_add">Toevoegen</string> <string name="lbl_add">Toevoegen</string>
<string name="lbl_save">Opslaan</string> <string name="lbl_save">Opslaan</string>
<string name="err_no_dirs">Geen Mappen</string> <string name="err_no_dirs">Geen mappen</string>
<string name="lbl_about">Over</string> <string name="lbl_about">Over</string>
<string name="lbl_version">Versie</string> <string name="lbl_version">Versie</string>
<string name="lbl_code">Bekijken op GitHub</string> <string name="lbl_code">Broncode</string>
<string name="lbl_licenses">Licenties</string> <string name="lbl_licenses">Licenties</string>
<string name="lng_author">Ontwikkeld door OxygenCobalt</string> <string name="lng_author">Ontwikkeld door Alexander Capehart</string>
<!-- Settings namespace | Settings-related labels --> <!-- Settings namespace | Settings-related labels -->
<string name="set_root_title">Instellingen</string> <string name="set_root_title">Instellingen</string>
<string name="set_ui">Uiterlijk</string> <string name="set_ui">Uiterlijk en gevoel</string>
<string name="set_theme">Thema</string> <string name="set_theme">Thema</string>
<string name="set_theme_auto">Automatisch</string> <string name="set_theme_auto">Automatisch</string>
<string name="set_theme_day">Licht</string> <string name="set_theme_day">Licht</string>
@ -60,36 +60,36 @@
<string name="err_no_music">Geen muziek aangetroffen</string> <string name="err_no_music">Geen muziek aangetroffen</string>
<string name="err_index_failed">Laden van muziek mislukt</string> <string name="err_index_failed">Laden van muziek mislukt</string>
<string name="err_no_perms">Auxio heeft toestemming nodig om uw muziekbibliotheek te lezen</string> <string name="err_no_perms">Auxio heeft toestemming nodig om uw muziekbibliotheek te lezen</string>
<string name="err_no_app">Geen app kan deze link openen</string> <string name="err_no_app">Geen app gevonden die deze taak kan uitvoeren</string>
<string name="err_bad_dir">Deze map wordt niet ondersteund</string> <string name="err_bad_dir">Deze map wordt niet ondersteund</string>
<!-- Hint Namespace | EditText Hints --> <!-- Hint Namespace | EditText Hints -->
<string name="lng_search_library">Zoek in uw bibliotheek…</string> <string name="lng_search_library">Zoek in uw bibliotheek…</string>
<!-- Description Namespace | Accessibility Strings --> <!-- Description Namespace | Accessibility Strings -->
<string name="desc_track_number">Nummer %d</string> <string name="desc_track_number">Nummer %d</string>
<string name="desc_play_pause">Afspelen/Pauzeren</string> <string name="desc_play_pause">Afspelen of pauzeren</string>
<string name="desc_skip_next">Naar volgend nummer gaan</string> <string name="desc_skip_next">Naar volgend nummer gaan</string>
<string name="desc_skip_prev">Naar het laatste nummer gaan</string> <string name="desc_skip_prev">Naar het laatste nummer gaan</string>
<string name="desc_change_repeat">Herhaalfunctie wijzigen</string> <string name="desc_change_repeat">Herhaalfunctie wijzigen</string>
<string name="desc_clear_search">Zoekopdracht wissen</string> <string name="desc_clear_search">Zoekopdracht wissen</string>
<string name="desc_music_dir_delete">Verwijder uitgesloten map</string> <string name="desc_music_dir_delete">Map verwijderen</string>
<string name="desc_auxio_icon">Auxio pictogram</string> <string name="desc_auxio_icon">Auxio pictogram</string>
<string name="desc_album_cover">Artist Image voor %s</string> <string name="desc_album_cover">Albumhoes voor %s</string>
<string name="desc_artist_image">Artist Image voor %s</string> <string name="desc_artist_image">Artiesten-afbeelding voor %s</string>
<string name="desc_genre_image">Genre Image voor %s</string> <string name="desc_genre_image">Genre-afbeelding voor %s</string>
<!-- Placeholder Namespace | Placeholder values --> <!-- Placeholder Namespace | Placeholder values -->
<string name="def_genre">Onbekend Genre</string> <string name="def_genre">Onbekend genre</string>
<string name="def_date">Geen datum </string> <string name="def_date">Geen datum</string>
<!-- Color Label namespace | Accent names --> <!-- Color Label namespace | Accent names -->
<string name="clr_red">Rood</string> <string name="clr_red">Rood</string>
<string name="clr_pink">Roze</string> <string name="clr_pink">Roze</string>
<string name="clr_purple">Paars</string> <string name="clr_purple">Paars</string>
<string name="clr_deep_purple">Donkerpaars</string> <string name="clr_deep_purple">Dieppaars</string>
<string name="clr_indigo">Indigoblauw</string> <string name="clr_indigo">Indigoblauw</string>
<string name="clr_blue">Blauw</string> <string name="clr_blue">Blauw</string>
<string name="clr_deep_blue">Donkerblauw</string> <string name="clr_deep_blue">Diepblauw</string>
<string name="clr_teal">Blauwgroen</string> <string name="clr_teal">Blauwgroen</string>
<string name="clr_green">Groen</string> <string name="clr_green">Groen</string>
<string name="clr_deep_green">Donkergroen</string> <string name="clr_deep_green">Diepgroen</string>
<string name="clr_cyan">Cyaan</string> <string name="clr_cyan">Cyaan</string>
<string name="clr_lime">Geelgroen</string> <string name="clr_lime">Geelgroen</string>
<string name="clr_yellow">Geel</string> <string name="clr_yellow">Geel</string>
@ -99,14 +99,14 @@
<!-- Format Namespace | Value formatting/plurals --> <!-- Format Namespace | Value formatting/plurals -->
<string name="fmt_lib_song_count">Nummers geladen: %d</string> <string name="fmt_lib_song_count">Nummers geladen: %d</string>
<plurals name="fmt_song_count"> <plurals name="fmt_song_count">
<item quantity="one">%d Nummer</item> <item quantity="one">%d lied</item>
<item quantity="other">%d Nummers</item> <item quantity="other">%d liedjes</item>
</plurals> </plurals>
<plurals name="fmt_album_count"> <plurals name="fmt_album_count">
<item quantity="one">%d Album</item> <item quantity="one">%d album</item>
<item quantity="other">%d Albums</item> <item quantity="other">%d albums</item>
</plurals> </plurals>
<string name="def_artist">Onbekend Artist</string> <string name="def_artist">Onbekende artiest</string>
<string name="set_black_mode">Zwart thema</string> <string name="set_black_mode">Zwart thema</string>
<string name="set_black_mode_desc">Gebruik een puur-zwart donker thema</string> <string name="set_black_mode_desc">Gebruik een puur-zwart donker thema</string>
<string name="set_repeat_pause">Pauze op herhaling</string> <string name="set_repeat_pause">Pauze op herhaling</string>
@ -118,7 +118,7 @@
<string name="lbl_song_detail">Bekijk eigenschappen</string> <string name="lbl_song_detail">Bekijk eigenschappen</string>
<string name="lbl_name">Naam</string> <string name="lbl_name">Naam</string>
<string name="lbl_artist">Artiest</string> <string name="lbl_artist">Artiest</string>
<string name="lbl_cancel">@android:string/cancel</string> <string name="lbl_cancel">Annuleren</string>
<string name="set_lib_tabs">Bibliotheek tabbladen</string> <string name="set_lib_tabs">Bibliotheek tabbladen</string>
<string name="lbl_date">Jaar</string> <string name="lbl_date">Jaar</string>
<string name="lbl_relative_path">Ouderpad</string> <string name="lbl_relative_path">Ouderpad</string>
@ -133,25 +133,25 @@
<string name="set_dirs_mode">Modus</string> <string name="set_dirs_mode">Modus</string>
<string name="set_dirs_desc">Bepaal waar muziek vandaan moet worden geladen</string> <string name="set_dirs_desc">Bepaal waar muziek vandaan moet worden geladen</string>
<string name="fmt_lib_total_duration">Totale duur: %s</string> <string name="fmt_lib_total_duration">Totale duur: %s</string>
<string name="lbl_shuffle_shortcut_long">Shuffle Alles</string> <string name="lbl_shuffle_shortcut_long">Alles schudden</string>
<string name="lbl_ok">@android:string/ok</string> <string name="lbl_ok">Oké</string>
<string name="set_headset_autoplay_desc">Altijd beginnen met spelen als een headset is aangesloten (werkt mogelijk niet op alle apparaten)</string> <string name="set_headset_autoplay_desc">Altijd beginnen met spelen als een headset is aangesloten (werkt mogelijk niet op alle apparaten)</string>
<string name="desc_shuffle">Schakel shuffle aan of uit</string> <string name="desc_shuffle">Schakel shuffle aan of uit</string>
<string name="set_reindex_desc">Kan afspeelstatus wissen</string> <string name="set_reindex_desc">De muziekbibliotheek opnieuw laden, indien mogelijk met behulp van tags uit het cachegeheugen</string>
<string name="fmt_indexing">Uw muziekbibliotheek wordt geladen… (%1$d/%2$d)</string> <string name="fmt_indexing">Uw muziekbibliotheek wordt geladen… (%1$d/%2$d)</string>
<string name="set_dirs_mode_exclude">Uitgezonderd</string> <string name="set_dirs_mode_exclude">Uitgezonderd</string>
<string name="desc_shuffle_all">Alle liedjes shuffelen</string> <string name="desc_shuffle_all">Alle liedjes shuffelen</string>
<string name="set_dirs_mode_include">Includeer</string> <string name="set_dirs_mode_include">Includeer</string>
<string name="set_repeat_pause_desc">Pauze wanneer een liedje wordt herhaald</string> <string name="set_repeat_pause_desc">Pauze wanneer een liedje wordt herhaald</string>
<string name="set_dirs_mode_exclude_desc">Muziek zal <b>niet</b> worden geladen vanuit de mappen die u toevoegt.</string> <string name="set_dirs_mode_exclude_desc">Muziek zal <b>niet</b> worden geladen vanuit de mappen die u toevoegt.</string>
<string name="set_reindex">Muziek herladen</string> <string name="set_reindex">Muziek verfrissen</string>
<string name="set_dirs_mode_include_desc">Muziek zal <b>alleen</b> worden geladen uit de mappen die u toevoegt.</string> <string name="set_dirs_mode_include_desc">Muziek zal <b>alleen</b> worden geladen uit de mappen die u toevoegt.</string>
<string name="set_pre_amp_with">Aanpassing met tags</string> <string name="set_pre_amp_with">Aanpassing met tags</string>
<string name="set_pre_amp_without">Aanpassing zonder tags</string> <string name="set_pre_amp_without">Aanpassing zonder tags</string>
<string name="def_playback">Er speelt geen muziek</string> <string name="def_playback">Er speelt geen muziek</string>
<string name="set_detail_song_playback_mode">Bij het afspelen van item details</string> <string name="set_detail_song_playback_mode">Bij het afspelen van item details</string>
<string name="set_round_mode">Ronde modus</string> <string name="set_round_mode">Ronde modus</string>
<string name="set_round_mode_desc">Afgeronde hoeken op extra UI-elementen inschakelen (vereist dat albumhoezen afgerond zijn)</string> <string name="set_round_mode_desc">Afgeronde hoeken inschakelen voor extra UI-elementen (vereist dat albumhoezen zijn afgerond)</string>
<string name="lbl_state_restored">Staat gerestaureerd</string> <string name="lbl_state_restored">Staat gerestaureerd</string>
<string name="lbl_library_counts">Bibliotheekstatistieken</string> <string name="lbl_library_counts">Bibliotheekstatistieken</string>
<string name="set_lib_tabs_desc">Verander de zichtbaarheid en volgorde van bibliotheek-tabbladen</string> <string name="set_lib_tabs_desc">Verander de zichtbaarheid en volgorde van bibliotheek-tabbladen</string>
@ -161,18 +161,18 @@
<string name="set_playback_mode_none">Afspelen vanaf getoond item</string> <string name="set_playback_mode_none">Afspelen vanaf getoond item</string>
<string name="set_restore_state">Afspeelstatus herstellen</string> <string name="set_restore_state">Afspeelstatus herstellen</string>
<string name="set_restore_desc">Herstel de eerder opgeslagen afspeelstatus (indien aanwezig)</string> <string name="set_restore_desc">Herstel de eerder opgeslagen afspeelstatus (indien aanwezig)</string>
<string name="err_did_not_restore">Geen staat kan hersteld worden</string> <string name="err_did_not_restore">Kan status niet herstellen</string>
<string name="desc_remove_song">Verwijder dit wachtrij liedje</string> <string name="desc_remove_song">Verwijder dit wachtrij liedje</string>
<string name="desc_song_handle">Verplaats dit wachtrij liedje</string> <string name="desc_song_handle">Verplaats dit wachtrij liedje</string>
<string name="desc_tab_handle">Verplaats deze tab</string> <string name="desc_tab_handle">Verplaats deze tab</string>
<string name="desc_no_cover">Album cover</string> <string name="desc_no_cover">Album cover</string>
<string name="def_track">Geen tracknummer</string> <string name="def_track">Geen nummer</string>
<string name="fmt_db_neg">-%.1f dB</string> <string name="fmt_db_neg">-%.1f dB</string>
<string name="clr_dynamic">Dynamisch</string> <string name="clr_dynamic">Dynamisch</string>
<string name="cdc_mp3">MPEG-1 Audio</string> <string name="cdc_mp3">MPEG-1 audio</string>
<string name="cdc_mp4">MPEG-4 Audio</string> <string name="cdc_mp4">MPEG-4-audio</string>
<string name="cdc_ogg">Ogg Audio</string> <string name="cdc_ogg">Ogg audio</string>
<string name="cdc_mka">Matroska Audio</string> <string name="cdc_mka">Matroska-audio</string>
<string name="fmt_lib_album_count">Albums geladen: %d</string> <string name="fmt_lib_album_count">Albums geladen: %d</string>
<string name="fmt_lib_artist_count">Artiesten geladen: %d</string> <string name="fmt_lib_artist_count">Artiesten geladen: %d</string>
<string name="fmt_lib_genre_count">Genres geladen: %d</string> <string name="fmt_lib_genre_count">Genres geladen: %d</string>
@ -189,4 +189,111 @@
<string name="lbl_shuffle_shortcut_short">Shuffle</string> <string name="lbl_shuffle_shortcut_short">Shuffle</string>
<string name="cdc_aac">Geavanceerde audio codering (GAC)</string> <string name="cdc_aac">Geavanceerde audio codering (GAC)</string>
<string name="cdc_flac">Gratis verliesvrije audiocodec (GVAC)</string> <string name="cdc_flac">Gratis verliesvrije audiocodec (GVAC)</string>
<string name="lbl_new_playlist">Nieuwe afspeellijst</string>
<string name="lbl_state_wiped">Afspeelstatus gewist</string>
<string name="set_audio_desc">Geluids- en afspeelgedrag configureren</string>
<string name="desc_new_playlist">Een nieuwe afspeellijst maken</string>
<string name="fmt_selected">%d Geselecteerd</string>
<string name="lbl_playlist_add">Toevoegen aan afspeellijst</string>
<string name="lng_playlist_created">Afspeellijst gemaakt</string>
<string name="lng_playlist_added">Toegevoegd aan afspeellijst</string>
<string name="set_ui_desc">Het thema en de kleuren van de app wijzigen</string>
<string name="set_personalize_desc">UI-besturingselementen en gedrag aanpassen</string>
<string name="set_content_desc">Bepaal hoe muziek en afbeeldingen worden geladen</string>
<string name="set_music">Muziek</string>
<string name="set_observing">Automatisch herladen</string>
<string name="set_separators_semicolon">Puntkomma (;)</string>
<string name="set_separators_comma">Komma (,)</string>
<string name="set_separators_plus">Plus (+)</string>
<string name="set_separators_warning">Waarschuwing: Het gebruik van deze instelling kan ertoe leiden dat sommige tags verkeerd geïnterpreteerd worden als tags met meerdere waarden. U kunt dit oplossen door ongewenste scheidingstekens vooraf te laten gaan door een backslash (\\).</string>
<string name="set_hide_collaborators_desc">Toon alleen artiesten die rechtstreeks op een album worden genoemd (werkt het beste op goed getagde bibliotheken)</string>
<string name="set_intelligent_sorting_desc">Sorteer namen die beginnen met cijfers of woorden zoals \"de\" correct (werkt het beste met Engelstalige muziek)</string>
<string name="desc_exit">Stop met afspelen</string>
<string name="lbl_play_selected">Geselecteerd afspelen</string>
<string name="lng_indexing">Uw muziekbibliotheek wordt geladen…</string>
<string name="set_behavior">Gedrag</string>
<string name="lbl_compilation_remix">Remix compilatie</string>
<string name="lbl_soundtrack">Soundtrack</string>
<string name="lbl_mixtape">Mixtape</string>
<string name="lbl_mix">DJ-mix</string>
<string name="lbl_remix_group">Remixen</string>
<string name="set_separators_slash">Schuine streep (/)</string>
<string name="set_replay_gain">WeergaveWinst</string>
<string name="set_state">Volharding</string>
<string name="lbl_playlist">Afspeellijst</string>
<string name="lbl_wiki">Wiki</string>
<string name="err_did_not_save">Kan status niet opslaan</string>
<string name="lbl_reset">Resetten</string>
<string name="set_images">Afbeeldingen</string>
<string name="set_wipe_state">Afspeelstatus wissen</string>
<string name="set_cover_mode_media_store">Snel</string>
<string name="set_library">Bibliotheek</string>
<string name="lbl_ep_live">Live EP</string>
<string name="lbl_ep_remix">Remix EP</string>
<string name="lbl_single_live">Live single</string>
<string name="lbl_single_remix">Remix single</string>
<string name="lbl_compilations">Compilaties</string>
<string name="lbl_compilation">Compilatie</string>
<string name="lbl_live_group">Live</string>
<string name="lbl_rename_playlist">Afspeellijst hernoemen</string>
<string name="lbl_confirm_delete_playlist">Afspeellijst verwijderen\?</string>
<string name="set_action_mode_repeat">Herhaalmodus</string>
<string name="set_dirs_list">Mappen</string>
<string name="desc_queue_bar">De wachtrij openen</string>
<string name="fmt_deletion_info">%s verwijderen\? Dit kan niet ongedaan worden gemaakt.</string>
<string name="set_cover_mode_off">Uit</string>
<string name="set_cover_mode_quality">Hoge kwaliteit</string>
<string name="lbl_genre">Genre</string>
<string name="set_separators_and">Ampersand (&amp;)</string>
<string name="lbl_edit">Bewerken</string>
<string name="lbl_sort_dec">Aflopend</string>
<string name="err_did_not_wipe">Kan status niet wissen</string>
<string name="desc_playlist_image">Afspeellijst-afbeelding voor %s</string>
<string name="def_song_count">Geen nummers</string>
<string name="lbl_equalizer">Gelijkmaker</string>
<string name="lbl_singles">Singles</string>
<string name="lbl_single">Single</string>
<string name="lbl_eps">EP\'s</string>
<string name="lbl_ep">EP</string>
<string name="lbl_album_live">Live album</string>
<string name="lbl_album_remix">Remix album</string>
<string name="lbl_soundtracks">Soundtracks</string>
<string name="lbl_mixtapes">Mixtapes</string>
<string name="lbl_indexing">Muziek laden</string>
<string name="lbl_observing">Muziekbibliotheek bewaken</string>
<string name="lbl_compilation_live">Live compilatie</string>
<string name="lbl_mixes">DJ-mixen</string>
<string name="lng_observing">Uw muziekbibliotheek controleren op wijzigingen…</string>
<string name="lng_playlist_renamed">Afspeellijst hernoemd</string>
<string name="lbl_rename">Hernoemen</string>
<string name="set_bar_action">Aangepaste afspeelbalkactie</string>
<string name="set_separators_desc">Tekens configureren die meerdere tagwaarden aanduiden</string>
<string name="lbl_delete">Verwijderen</string>
<string name="set_separators">Scheiders met meerdere waarden</string>
<string name="set_hide_collaborators">Verberg bijdragers</string>
<string name="set_playback_mode_genre">Speel vanuit genre</string>
<string name="lbl_date_added">Datum toegevoegd</string>
<string name="fmt_list">%1$s, %2$s</string>
<string name="fmt_def_playlist">Afspeellijst %d</string>
<plurals name="fmt_artist_count">
<item quantity="one">%d artiest</item>
<item quantity="other">%d artiesten</item>
</plurals>
<string name="lbl_shuffle_selected">Shuffle geselecteerd</string>
<string name="set_intelligent_sorting">Intelligent sorteren</string>
<string name="lbl_appears_on">Verschijnt op</string>
<string name="lbl_playlists">Afspeellijsten</string>
<string name="lbl_share">Delen</string>
<string name="lng_playlist_deleted">Afspeellijst verwijderd</string>
<string name="set_action_mode_next">Naar volgende</string>
<string name="set_observing_desc">Laad de muziekbibliotheek opnieuw wanneer deze wordt gewijzigd (vereist permanente melding)</string>
<string name="set_exclude_non_music">Niet-muziek uitsluiten</string>
<string name="set_exclude_non_music_desc">Negeer audiobestanden die geen muziek zijn, zoals podcasts</string>
<string name="set_cover_mode">Albumhoezen</string>
<string name="set_playback">Afspeel</string>
<string name="set_rescan">Muziek opnieuw scannen</string>
<string name="set_rescan_desc">De tag-cache wissen en de muziekbibliotheek volledig opnieuw laden (langzamer, maar vollediger)</string>
<string name="set_wipe_desc">De eerder opgeslagen afspeelstatus wissen (indien aanwezig)</string>
<string name="def_disc">Geen schijf</string>
<string name="fmt_editing">%s aan het bewerken</string>
</resources> </resources>

View file

@ -27,8 +27,8 @@
<string name="lbl_soundtrack">ਸਾਊਂਡਟ੍ਰੈਕ</string> <string name="lbl_soundtrack">ਸਾਊਂਡਟ੍ਰੈਕ</string>
<string name="lbl_soundtracks">ਸਾਊਂਡਟ੍ਰੈਕਸ</string> <string name="lbl_soundtracks">ਸਾਊਂਡਟ੍ਰੈਕਸ</string>
<string name="lbl_mixtapes">ਮਿਕਸਟੇਪਸ</string> <string name="lbl_mixtapes">ਮਿਕਸਟੇਪਸ</string>
<string name="lbl_mixes">ਮਿਕਸ</string> <string name="lbl_mixes">ਡੀਜੇ ਮਿਕਸ</string>
<string name="lbl_mix">ਮਿਕਸ</string> <string name="lbl_mix">ਡੀਜੇ ਮਿਕਸ</string>
<string name="lbl_live_group">ਲਾਈਵ</string> <string name="lbl_live_group">ਲਾਈਵ</string>
<string name="lbl_remix_group">ਰੀਮਿਕਸ</string> <string name="lbl_remix_group">ਰੀਮਿਕਸ</string>
<string name="lbl_artist">ਕਲਾਕਾਰ</string> <string name="lbl_artist">ਕਲਾਕਾਰ</string>
@ -180,7 +180,7 @@
<string name="set_separators_comma">ਕੌਮਾ (,)</string> <string name="set_separators_comma">ਕੌਮਾ (,)</string>
<string name="set_separators_semicolon">ਸੈਮੀਕੋਲਨ (;)</string> <string name="set_separators_semicolon">ਸੈਮੀਕੋਲਨ (;)</string>
<string name="set_separators_slash">ਸਲੈਸ਼ (/)</string> <string name="set_separators_slash">ਸਲੈਸ਼ (/)</string>
<string name="set_separators_and">ਐਂਪਰਸੈਂਡ (&amp;)</string> <string name="set_separators_and">Ampersand (&amp;)</string>
<string name="set_hide_collaborators">ਸਹਿਯੋਗੀਆਂ ਨੂੰ ਲੁਕਾਓ</string> <string name="set_hide_collaborators">ਸਹਿਯੋਗੀਆਂ ਨੂੰ ਲੁਕਾਓ</string>
<string name="set_audio_desc">ਆਵਾਜ਼ ਅਤੇ ਪਲੇਬੈਕ ਵਿਵਹਾਰ ਦੀ ਸੰਰਚਨਾ ਕਰੋ</string> <string name="set_audio_desc">ਆਵਾਜ਼ ਅਤੇ ਪਲੇਬੈਕ ਵਿਵਹਾਰ ਦੀ ਸੰਰਚਨਾ ਕਰੋ</string>
<string name="set_playback">ਪਲੇਅਬੈਕ</string> <string name="set_playback">ਪਲੇਅਬੈਕ</string>
@ -202,4 +202,89 @@
<string name="desc_shuffle">ਸ਼ਫਲ ਚਾਲੂ ਜਾਂ ਬੰਦ ਕਰੋ</string> <string name="desc_shuffle">ਸ਼ਫਲ ਚਾਲੂ ਜਾਂ ਬੰਦ ਕਰੋ</string>
<string name="desc_shuffle_all">ਸਾਰੇ ਗੀਤਾਂ ਨੂੰ ਸ਼ਫਲ ਕਰੋ</string> <string name="desc_shuffle_all">ਸਾਰੇ ਗੀਤਾਂ ਨੂੰ ਸ਼ਫਲ ਕਰੋ</string>
<string name="desc_exit">ਪਲੇਬੈਕ ਬੰਦ ਕਰੋ</string> <string name="desc_exit">ਪਲੇਬੈਕ ਬੰਦ ਕਰੋ</string>
<string name="lbl_new_playlist">ਨਵੀਂ ਪਲੇਅ-ਲਿਸਟ</string>
<string name="lbl_playlist_add">ਪਲੇਅ-ਲਿਸਟ ਵਿੱਚ ਜੋੜ੍ਹੋ</string>
<string name="fmt_list">%1$s, %2$s</string>
<string name="clr_teal">ਫਿੱਕਾ ਨੀਲਾ-ਹਰਾ</string>
<string name="lbl_playlist">ਪਲੇਅ-ਲਿਸਟ</string>
<string name="lng_playlist_deleted">ਪਲੇਅ-ਲਿਸਟ ਮਿਟਾਈ</string>
<string name="lng_playlist_created">ਪਲੇਅ-ਲਿਸਟ ਬਣ ਗਈ</string>
<string name="lng_playlist_renamed">ਪਲੇਅ-ਲਿਸਟ ਦਾ ਨਾਂ ਬਦਲਿਆ</string>
<string name="clr_cyan">ਨੀਲਾ-ਹਰਾ</string>
<string name="lbl_delete">ਮਿਟਾਓ</string>
<string name="desc_new_playlist">ਇੱਕ ਨਵੀਂ ਪਲੇਅ-ਲਿਸਟ ਬਣਾਓ</string>
<string name="lng_playlist_added">ਪਲੇਅ- ਲਿਸਟ ਵਿੱਚ ਸ਼ਾਮਿਲ ਕੀਤਾ</string>
<string name="desc_tab_handle">ਇਹ ਟੈਬ ਹਿਲਾਓ</string>
<string name="def_song_count">ਕੋਈ ਗੀਤ ਨਹੀਂ</string>
<string name="cdc_mka">Matroska ਆਡੀਓ</string>
<string name="clr_deep_purple">ਗੂੜ੍ਹਾ ਜ੍ਹਾਮਣੀ</string>
<string name="cdc_ogg">Ogg ਆਡੀਓ</string>
<string name="fmt_lib_song_count">% d: ਗੀਤ ਲੋਡ ਕੀਤੇ</string>
<plurals name="fmt_album_count">
<item quantity="one">%d ਐਲਬਮ</item>
<item quantity="other">%d ਐਲਬਮਾਂ</item>
</plurals>
<string name="lbl_rename">ਨਾਂ ਬਦਲੋ</string>
<string name="lbl_rename_playlist">ਪਲੇਅ-ਲਿਸਟ ਦਾ ਨਾਂ ਬਦਲੋ</string>
<string name="lbl_confirm_delete_playlist">ਪਲੇਅ-ਲਿਸਟ ਮਿਟਾਓ\?</string>
<string name="set_intelligent_sorting">ਸੂਝ-ਬੂਝ ਨਾਲ ਲੜੀਬੱਧ</string>
<string name="set_intelligent_sorting_desc">ਉਹਨਾਂ ਨਾਵਾਂ ਨੂੰ ਠੀਕ ਤਰ੍ਹਾਂ ਲੜੀਬੱਧ ਕਰੋ ਜੋ ਨੰਬਰਾਂ ਜਾਂ ਸ਼ਬਦਾਂ ਨਾਲ ਸ਼ੁਰੂ ਹੁੰਦੇ ਹਨ ਜਿਵੇਂ ਕਿ \"ਦਾ\" (ਅੰਗਰੇਜ਼ੀ-ਭਾਸ਼ਾ ਦੇ ਸੰਗੀਤ ਦੇ ਨਾਲ ਸਭ ਤੋਂ ਵਧੀਆ ਕੰਮ ਕਰਦਾ ਹੈ)</string>
<string name="fmt_deletion_info">%s ਹਟਾਉਣਾ ਹੈ\? ਇਸ ਨੂੰ ਅਣਕੀਤਾ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਦਾ।</string>
<string name="clr_deep_blue">ਗੂੜ੍ਹਾ ਨੀਲਾ</string>
<plurals name="fmt_artist_count">
<item quantity="one">%d ਕਲਾਕਾਰ</item>
<item quantity="other">%d ਕਲਾਕਾਰ</item>
</plurals>
<string name="fmt_disc_no">ਡਿਸਕ %d</string>
<string name="clr_blue">ਨੀਲਾ</string>
<string name="fmt_def_playlist">ਪਲੇਅ-ਲਿਸਟ %d</string>
<string name="cdc_mp3">MPEG-1 ਆਡੀਓ</string>
<string name="cdc_mp4">MPEG-4 ਆਡੀਓ</string>
<string name="fmt_selected">%d ਚੁਣੇ</string>
<string name="clr_green">ਹਰਾ</string>
<string name="desc_clear_search">ਖੋਜ ਕਰੀ ਸਾਫ਼ ਕਰੋ</string>
<string name="def_track">ਕੋਈ ਟਰੈਕ ਨਹੀਂ</string>
<string name="clr_pink">ਗੁਲਾਬੀ</string>
<string name="clr_purple">ਜ੍ਹਾਮਣੀ</string>
<string name="cdc_aac">ਅਡਵਾਂਸਡ ਆਡੀਓ ਕੋਡਿੰਗ (AAC)</string>
<string name="cdc_flac">ਫਰੀ ਲੂਜ਼ਲੈੱਸ ਆਡੀਓ Codec (FLAC)</string>
<string name="clr_red">ਲਾਲ</string>
<string name="clr_orange">ਸੰਤਰੀ</string>
<string name="fmt_lib_genre_count">%d: ਸ਼ੈਲੀਆਂ ਲੋਡ ਕੀਤੀਆਂ</string>
<string name="fmt_lib_album_count">%d: ਐਲਬਮ ਲੋਡ ਕੀਤੇ</string>
<string name="fmt_lib_artist_count">%d: ਕਲਾਕਾਰ ਲੋਡ ਕੀਤੇ</string>
<string name="lbl_edit">ਸੋਧ ਕਰੋ</string>
<string name="desc_album_cover">%s ਲਈ ਐਲਬਮ ਕਵਰ</string>
<string name="desc_no_cover">ਐਲਬਮ ਕਵਰ</string>
<string name="desc_artist_image">%s ਲਈ ਕਲਾਕਾਰ ਚਿੱਤਰ</string>
<string name="desc_genre_image">%s ਲਈ ਸ਼ੈਲੀ ਚਿੱਤਰ</string>
<string name="desc_playlist_image">%s ਲਈ ਪਲੇਅ-ਲਿਸਟ ਚਿੱਤਰ</string>
<string name="def_artist">ਅਣਜਾਣ ਕਲਾਕਾਰ</string>
<string name="def_genre">ਅਣਜਾਣ ਸ਼ੈਲੀ</string>
<string name="def_date">ਕੋਈ ਮਿਤੀ ਨਹੀਂ</string>
<string name="def_playback">ਕੋਈ ਸੰਗੀਤ ਨਹੀਂ ਚੱਲ ਰਿਹਾ</string>
<string name="clr_deep_green">ਗੂੜ੍ਹਾ ਹਰਾ</string>
<string name="clr_yellow">ਪੀਲ੍ਹਾ</string>
<string name="clr_lime">ਨਿੰਬੂ ਰੰਗਾ</string>
<string name="fmt_db_pos">+%.1f dB</string>
<string name="fmt_bitrate">%d kbps</string>
<string name="fmt_sample_rate">%d Hz</string>
<string name="clr_brown">ਭੂਰਾ</string>
<string name="fmt_db_neg">-%.1f dB</string>
<plurals name="fmt_song_count">
<item quantity="one">%d ਗੀਤ</item>
<item quantity="other">%d ਗੀਤ</item>
</plurals>
<string name="clr_grey">ਸਲੇਟੀ</string>
<string name="fmt_lib_total_duration">ਕੁੱਲ ਮਿਆਦ: %s</string>
<string name="desc_music_dir_delete">ਫੋਲਡਰ ਹਟਾਓ</string>
<string name="desc_auxio_icon">Auxio ਆਈਕਾਨ</string>
<string name="lbl_appears_on">ਉੱਤੇ ਵਿਖਾਈ ਦਿੰਦਾ ਹੈ</string>
<string name="lbl_playlists">ਪਲੇਅ-ਲਿਸਟਾਂ</string>
<string name="lbl_share">ਸਾਂਝਾ ਕਰੋ</string>
<string name="def_disc">ਕੋਈ ਡਿਸਕ ਨਹੀਂ</string>
<string name="clr_indigo">ਬੈਂਗਣੀਂ</string>
<string name="clr_dynamic">ਡਾਇਨੈਮਿਕ</string>
<string name="fmt_editing">%s ਸੋਧ ਰਿਹਾ</string>
<string name="fmt_indexing">ਤੁਹਾਡੀ ਸੰਗੀਤ ਲਾਇਬਰੇਰੀ ਲੋਡ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ… (%1$d/%2$d)</string>
</resources> </resources>

View file

@ -191,7 +191,7 @@
<string name="lbl_album_live">Концертный альбом</string> <string name="lbl_album_live">Концертный альбом</string>
<string name="lbl_live_group">Концертный</string> <string name="lbl_live_group">Концертный</string>
<string name="lng_observing">Мониторинг изменений в музыкальной библиотеке…</string> <string name="lng_observing">Мониторинг изменений в музыкальной библиотеке…</string>
<string name="lbl_state_wiped">Позиция очищена</string> <string name="lbl_state_wiped">Позиция сброшена</string>
<string name="set_dirs">Папки с музыкой</string> <string name="set_dirs">Папки с музыкой</string>
<string name="set_dirs_mode_include">Включить</string> <string name="set_dirs_mode_include">Включить</string>
<string name="lbl_album_remix">Альбом ремиксов</string> <string name="lbl_album_remix">Альбом ремиксов</string>

View file

@ -275,4 +275,9 @@
<string name="set_intelligent_sorting">Sıralama yaparken makaleleri yoksay</string> <string name="set_intelligent_sorting">Sıralama yaparken makaleleri yoksay</string>
<string name="set_intelligent_sorting_desc">Ada göre sıralarken \"the\" gibi kelimeleri yok sayın (en iyi ingilizce müzikle çalışır)</string> <string name="set_intelligent_sorting_desc">Ada göre sıralarken \"the\" gibi kelimeleri yok sayın (en iyi ingilizce müzikle çalışır)</string>
<string name="desc_new_playlist">Yeni bir oynatma listesi oluştur</string> <string name="desc_new_playlist">Yeni bir oynatma listesi oluştur</string>
<string name="lbl_new_playlist">Yeni Oynatma Listesi</string>
<string name="lbl_delete">Sil</string>
<string name="lbl_rename">Yeniden Adlandır</string>
<string name="lbl_rename_playlist">Oynatma Listesini Yeniden Adlandır</string>
<string name="lbl_confirm_delete_playlist">Oynatma listesini silmek istiyor musun\?</string>
</resources> </resources>

View file

@ -27,7 +27,7 @@
<string name="lbl_licenses">Ліцензії</string> <string name="lbl_licenses">Ліцензії</string>
<!-- Settings namespace | Settings-related labels --> <!-- Settings namespace | Settings-related labels -->
<string name="set_root_title">Налаштування</string> <string name="set_root_title">Налаштування</string>
<string name="set_ui">Вигляд і поведінка</string> <string name="set_ui">Вигляд</string>
<string name="set_theme">Тема</string> <string name="set_theme">Тема</string>
<string name="set_theme_day">Світла</string> <string name="set_theme_day">Світла</string>
<string name="set_theme_night">Темна</string> <string name="set_theme_night">Темна</string>

View file

@ -1,2 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<full-backup-content /> <full-backup-content>
<!-- Cache databases are useless between devices, drop them -->
<exclude domain="database" path="music_cache.db" />
</full-backup-content>

View file

@ -1,5 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<data-extraction-rules> <data-extraction-rules>
<cloud-backup /> <!-- Cache databases are useless between devices, drop them -->
<device-transfer /> <cloud-backup>
<exclude domain="database" path="music_cache.db" />
</cloud-backup>
<device-transfer>
<exclude domain="database" path="music_cache.db" />
</device-transfer>
</data-extraction-rules> </data-extraction-rules>

View file

@ -0,0 +1,3 @@
Auxio 3.1.0 introduces playlisting functionality, with more features coming soon.
This release adds some minor UI fixes and quality of life improvements.
For more information, see https://github.com/OxygenCobalt/Auxio/releases/tag/v3.1.2.

View file

@ -0,0 +1 @@
Yksinkertainen ja rationaallinen musiikkisoitin

View file

@ -10,7 +10,8 @@ Auxio एक तेज़, विश्वसनीय UI/UX वाला एक
- डिस्क संख्या, एकाधिक कलाकार, रिलीज़ प्रकार, सटीक के लिए समर्थन / मूल दिनांक, सॉर्ट टैग, और बहुत कुछ - डिस्क संख्या, एकाधिक कलाकार, रिलीज़ प्रकार, सटीक के लिए समर्थन / मूल दिनांक, सॉर्ट टैग, और बहुत कुछ
- उन्नत कलाकार प्रणाली जो कलाकारों और एल्बम कलाकारों को एकजुट करती है - उन्नत कलाकार प्रणाली जो कलाकारों और एल्बम कलाकारों को एकजुट करती है
- एसडी कार्ड-जागरूक फ़ोल्डर प्रबंधन - एसडी कार्ड-जागरूक फ़ोल्डर प्रबंधन
- विश्वसनीय प्लेबैक स्थिति दृढ़ता - विश्वसनीय प्लेलिस्टिंग कार्यक्षमता
- प्लेबैक अवस्था दृढ़ता
- पूर्ण रीप्लेगैन समर्थन (MP3, FLAC, OGG, OPUS और MP4 फ़ाइलों पर) - पूर्ण रीप्लेगैन समर्थन (MP3, FLAC, OGG, OPUS और MP4 फ़ाइलों पर)
- बाहरी तुल्यकारक समर्थन (उदा। वेवलेट) - बाहरी तुल्यकारक समर्थन (उदा। वेवलेट)
- एज-टू-एज - एज-टू-एज

View file

@ -1,22 +1,23 @@
Auxio je lokalni izvođač glazbe s brzim i pouzdanim korisničkim sučeljem/korisničkim iskustvom bez nepotrebnih značajki koje su prisutne u ostalim izvođačima glazbe. Kreiran od Exoplayera, Auxio ima vrhunsku podršku za biblioteke i kvalitetu slušanja u usporedbi s drugim aplikacijama koje koriste zastarjelu funkcionalnost Androida. Ukratko, <b>reproducira glazbu.</b> Auxio je lokalni glazbeni player s brzim, pouzdanim UI/UX bez mnogih beskorisnih značajki prisutnih u drugim glazbenim playerima. Izgrađen na temelju ExoPlayera, Auxio ima vrhunsku podršku za biblioteku i kvalitetu slušanja u usporedbi s drugim aplikacijama koje koriste zastarjelu funkcionalnost Androida. Ukratko, <b>Reproducira glazbu</b>.
<b>Značajke</b> <b>Značajke</b>
- Reprodukcija bazirana na ExoPlayeru - Reprodukcija temeljena na ExoPlayeru
- Brzo korisničko sučelje u skladu s najnovijim Materijal dizajnom - Snappy UI izvedeno iz najnovijih smjernica za materijalni dizajn
- Korisničko iskustvo koje priorizira jednostavnost korištenja - Iskustveni korisnički doživljaj koji daje prednost jednostavnosti upotrebe u odnosu na rubne slučajeve
- Prilagodljive ponašanje aplikacije - Prilagodljivo ponašanje
- Podrška za brojeve diskova, izvođače, vrste izdanja, - Podrška za brojeve diskova, više izvođača, vrste izdanja,
precizne/izvorne datume, oznake razvrstavanje i još više precizni/izvorni datumi, sortiranje oznaka i više
- Napredni sustav izvođača koji ujedinjuje izvođače i izvođače albuma - Napredni sustav izvođača koji ujedinjuje izvođače i izvođače albuma
- Upravljanje mapama SD kartica - Upravljanje mapama koje podržava SD karticu
- Pouzdana postojanost stanja reprodukcije - Pouzdana funkcija popisa pjesama
- Potpuna ReplayGain podrška (Za MP3, MP4, FLAC, OGG, i OPUS formate) - Postojanost stanja reprodukcije
- Podrška za eksterne ekvilajzere (npr. Wavelet) - Puna podrška za ReplayGain (na MP3, FLAC, OGG, OPUS i MP4 datotekama)
- Prikaz od ruba do ruba - Podrška za vanjski ekvilizator (npr. Wavelet)
- Podrška za ugrađene omote - Od ruba do ruba
- Pretražinje - Podrška za ugrađene naslovnice
- Mogućnost pokretanja glazbe čim spojite slušalice - Funkcionalnost pretraživanja
- Stilizirani widgeti koji automatski prilagođavaju svoju veličinu - Automatska reprodukcija slušalica
- Potpuno privatan bez potrebe za internetskom vezom - Elegantni widgeti koji se automatski prilagođavaju njihovoj veličini
- Bez zaobljenih omota albuma (Osim ako ih želite. Onda ih možete imati.) - Potpuno privatno i izvan mreže
- Nema zaobljenih naslovnica albuma (Osim ako ih ne želite. Onda možete.)

View file

@ -6,12 +6,14 @@ Auxio는 다른 음악 플레이어에 존재하는 많은 쓸모없는 기능
- 최신주목할 만한 디자인 가이드라인에서 파생된 Snappy UI - 최신주목할 만한 디자인 가이드라인에서 파생된 Snappy UI
- 엣지 케이스보다 사용 편의성을 우선시하는 의견이 많은 UX - 엣지 케이스보다 사용 편의성을 우선시하는 의견이 많은 UX
- 사용자 정의 가능한 동작 - 사용자 정의 가능한 동작
- 올바른 메타데이터의 우선 순위를 지정하는 고급 미디어 인덱서 - 디스크 번호, 여러 아티스트, 릴리스 유형 지원,
- 정확한/원래 날짜, 정렬 태그 및 릴리스 유형 지원(실험적) 정확한/원본 날짜, 정렬 태그 등 지원
- 아티스트와 앨범 아티스트를 통합하는 고급 아티스트 시스템
- SD 카드 인식 폴더 관리 - SD 카드 인식 폴더 관리
- 안정적인 재생 상태 지속성 - 안정적인 재생 목록 기능
- 완전한 ReplayGain 지원 (MP3, MP4, FLAC, OGG, OPUS) - 재생 상태 지속성
- 외부 이퀄라이저 기능 (Wavelet과 같은 앱) - 전체 ReplayGain 지원 (MP3, FLAC, OGG, OPUS, MP4)
- 외부 이퀄라이저 지원 (예: Wavelet)
- Edge-to-edge - Edge-to-edge
- 임베디드 커버 지원 - 임베디드 커버 지원
- 검색 기능 - 검색 기능

View file

@ -8,7 +8,10 @@ Auxio ਇੱਕ ਤੇਜ਼, ਭਰੋਸੇਮੰਦ UI/UX ਵਾਲਾ ਇੱ
- ਅਨੁਕੂਲਿਤ ਵਿਵਹਾਰ - ਅਨੁਕੂਲਿਤ ਵਿਵਹਾਰ
- ਡਿਸਕ ਨੰਬਰਾਂ, ਮਲਟੀਪਲ ਕਲਾਕਾਰਾਂ, ਰੀਲੀਜ਼ ਕਿਸਮਾਂ, ਸਟੀਕ ਲਈ ਸਮਰਥਨ /ਮੂਲ ਤਾਰੀਖਾਂ, ਕ੍ਰਮਬੱਧ ਟੈਗਸ, ਅਤੇ ਹੋਰ - ਡਿਸਕ ਨੰਬਰਾਂ, ਮਲਟੀਪਲ ਕਲਾਕਾਰਾਂ, ਰੀਲੀਜ਼ ਕਿਸਮਾਂ, ਸਟੀਕ ਲਈ ਸਮਰਥਨ /ਮੂਲ ਤਾਰੀਖਾਂ, ਕ੍ਰਮਬੱਧ ਟੈਗਸ, ਅਤੇ ਹੋਰ
- ਉੱਨਤ ਕਲਾਕਾਰ ਪ੍ਰਣਾਲੀ ਜੋ ਕਲਾਕਾਰਾਂ ਅਤੇ ਐਲਬਮ ਕਲਾਕਾਰਾਂ ਨੂੰ ਇਕਜੁੱਟ ਕਰਦੀ ਹੈ - ਉੱਨਤ ਕਲਾਕਾਰ ਪ੍ਰਣਾਲੀ ਜੋ ਕਲਾਕਾਰਾਂ ਅਤੇ ਐਲਬਮ ਕਲਾਕਾਰਾਂ ਨੂੰ ਇਕਜੁੱਟ ਕਰਦੀ ਹੈ
- SD ਕਾਰਡ-ਜਾਣੂ ਫੋਲਡਰ ਪ੍ਰਬੰਧਨ - ਭਰੋਸੇਯੋਗ ਪਲੇਬੈਕ ਸਥਿਤੀ ਸਥਿਰਤਾ - ਪੂਰਾ ਰੀਪਲੇਗੇਨ ਸਮਰਥਨ (MP3, FLAC, OGG, OPUS, ਅਤੇ MP4 ਫਾਈਲਾਂ 'ਤੇ) - SD ਕਾਰਡ-ਜਾਣੂ ਫੋਲਡਰ ਪ੍ਰਬੰਧਨ
- ਭਰੋਸੇਯੋਗ ਪਲੇਅਲਿਸਟਿੰਗ ਕਾਰਜਕੁਸ਼ਲਤਾ
- ਭਰੋਸੇਯੋਗ ਪਲੇਅਬੈਕ ਸਥਿਤੀ ਸਥਿਰਤਾ
- ਪੂਰਾ ਰੀਪਲੇਗੇਨ ਸਮਰਥਨ (MP3, FLAC, OGG, OPUS, ਅਤੇ MP4 ਫਾਈਲਾਂ 'ਤੇ)
- ਬਾਹਰੀ ਈਕੋਲਾਈਜ਼ਰ ਦਾ ਸਮਰਥਨ (ਉਦਾਹਰਨ. ਵੇਵਲੇਟ) - ਬਾਹਰੀ ਈਕੋਲਾਈਜ਼ਰ ਦਾ ਸਮਰਥਨ (ਉਦਾਹਰਨ. ਵੇਵਲੇਟ)
- ਕਿਨਾਰੇ-ਤੋਂ-ਕਿਨਾਰੇ - ਕਿਨਾਰੇ-ਤੋਂ-ਕਿਨਾਰੇ
- ਏਮਬੈਡਡ ਕਵਰ ਸਪੋਰਟ - ਏਮਬੈਡਡ ਕਵਰ ਸਪੋਰਟ