commit
81b030bfec
56 changed files with 1217 additions and 620 deletions
15
CHANGELOG.md
15
CHANGELOG.md
|
@ -1,5 +1,20 @@
|
|||
# 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
|
||||
|
||||
#### What's New
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
<h1 align="center"><b>Auxio</b></h1>
|
||||
<h4 align="center">A simple, rational music player for android.</h4>
|
||||
<p align="center">
|
||||
<a href="https://github.com/oxygencobalt/Auxio/releases/tag/v3.1.1">
|
||||
<img alt="Latest Version" src="https://img.shields.io/static/v1?label=tag&message=v3.1.1&color=64B5F6&style=flat">
|
||||
<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.2&color=64B5F6&style=flat">
|
||||
</a>
|
||||
<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">
|
||||
|
|
|
@ -20,8 +20,8 @@ android {
|
|||
|
||||
defaultConfig {
|
||||
applicationId namespace
|
||||
versionName "3.1.1"
|
||||
versionCode 31
|
||||
versionName "3.1.2"
|
||||
versionCode 32
|
||||
|
||||
minSdk 24
|
||||
targetSdk 33
|
||||
|
@ -56,6 +56,7 @@ android {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
jniLibs {
|
||||
excludes += ['**/kotlin/**', '**/okhttp3/**']
|
||||
|
@ -65,7 +66,6 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
|
|
|
@ -24,12 +24,12 @@
|
|||
android:name=".Auxio"
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="@xml/backup_descriptor"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/info_app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Auxio.App"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:appCategory="audio"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
tools:ignore="UnusedAttribute">
|
||||
|
|
|
@ -149,7 +149,7 @@ class MainFragment :
|
|||
logD("Configuring stacked bottom sheets")
|
||||
unlikelyToBeNull(binding.queueHandleWrapper).setOnClickListener {
|
||||
if (playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED &&
|
||||
queueSheetBehavior.state == BackportBottomSheetBehavior.STATE_COLLAPSED) {
|
||||
queueSheetBehavior.targetState == BackportBottomSheetBehavior.STATE_COLLAPSED) {
|
||||
// Playback sheet is expanded and queue sheet is collapsed, we can expand it.
|
||||
queueSheetBehavior.state = BackportBottomSheetBehavior.STATE_EXPANDED
|
||||
}
|
||||
|
@ -414,7 +414,7 @@ class MainFragment :
|
|||
val playbackSheetBehavior =
|
||||
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
|
||||
|
||||
if (playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_COLLAPSED) {
|
||||
if (playbackSheetBehavior.targetState == BackportBottomSheetBehavior.STATE_COLLAPSED) {
|
||||
// Playback sheet is not expanded and not hidden, we can expand it.
|
||||
logD("Expanding playback sheet")
|
||||
playbackSheetBehavior.state = BackportBottomSheetBehavior.STATE_EXPANDED
|
||||
|
@ -424,9 +424,9 @@ class MainFragment :
|
|||
val queueSheetBehavior =
|
||||
(binding.queueSheet.coordinatorLayoutBehavior ?: return) as QueueBottomSheetBehavior
|
||||
if (playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED &&
|
||||
queueSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED) {
|
||||
queueSheetBehavior.targetState == BackportBottomSheetBehavior.STATE_EXPANDED) {
|
||||
// Queue sheet and playback sheet is expanded, close the queue sheet so the
|
||||
// playback panel can eb shown.
|
||||
// playback panel can shown.
|
||||
logD("Collapsing queue sheet")
|
||||
queueSheetBehavior.state = BackportBottomSheetBehavior.STATE_COLLAPSED
|
||||
}
|
||||
|
@ -436,7 +436,7 @@ class MainFragment :
|
|||
val binding = requireBinding()
|
||||
val playbackSheetBehavior =
|
||||
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
|
||||
if (playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED) {
|
||||
if (playbackSheetBehavior.targetState == BackportBottomSheetBehavior.STATE_EXPANDED) {
|
||||
// Playback sheet (and possibly queue) needs to be collapsed.
|
||||
logD("Collapsing playback and queue sheets")
|
||||
val queueSheetBehavior =
|
||||
|
@ -487,8 +487,6 @@ class MainFragment :
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Use targetState more
|
||||
|
||||
private class SheetBackPressedCallback(
|
||||
private val playbackSheetBehavior: PlaybackBottomSheetBehavior<*>,
|
||||
private val queueSheetBehavior: QueueBottomSheetBehavior<*>?
|
||||
|
|
|
@ -480,7 +480,7 @@ constructor(
|
|||
// implicit album list into the mapping.
|
||||
logD("Implicit albums present, adding to list")
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(grouping as MutableMap<AlbumGrouping, List<Album>>)[AlbumGrouping.APPEARANCES] =
|
||||
(grouping as MutableMap<AlbumGrouping, Collection<Album>>)[AlbumGrouping.APPEARANCES] =
|
||||
artist.implicitAlbums
|
||||
}
|
||||
|
||||
|
@ -490,7 +490,7 @@ constructor(
|
|||
val header = BasicHeader(entry.key.headerTitleRes)
|
||||
list.add(Divider(header))
|
||||
list.add(header)
|
||||
list.addAll(entry.value)
|
||||
list.addAll(ARTIST_ALBUM_SORT.albums(entry.value))
|
||||
}
|
||||
|
||||
// Artists may not be linked to any songs, only include a header entry if we have any.
|
||||
|
@ -519,7 +519,7 @@ constructor(
|
|||
val artistHeader = BasicHeader(R.string.lbl_artists)
|
||||
list.add(Divider(artistHeader))
|
||||
list.add(artistHeader)
|
||||
list.addAll(genre.artists)
|
||||
list.addAll(GENRE_ARTIST_SORT.artists(genre.artists))
|
||||
|
||||
val songHeader = SortHeader(R.string.lbl_songs)
|
||||
list.add(Divider(songHeader))
|
||||
|
@ -576,4 +576,9 @@ constructor(
|
|||
LIVE(R.string.lbl_live_group),
|
||||
REMIXES(R.string.lbl_remix_group),
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val ARTIST_ALBUM_SORT = Sort(Sort.Mode.ByDate, Sort.Direction.DESCENDING)
|
||||
val GENRE_ARTIST_SORT = Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -115,7 +115,7 @@ private class DiscHeaderViewHolder(private val binding: ItemDiscHeaderBinding) :
|
|||
binding.discNumber.text = binding.context.getString(R.string.fmt_disc_no, disc.number)
|
||||
binding.discName.apply {
|
||||
text = disc.name
|
||||
isGone = text == null
|
||||
isGone = disc.name == null
|
||||
}
|
||||
} else {
|
||||
logD("Disc is null, defaulting to no disc")
|
||||
|
|
|
@ -52,6 +52,7 @@ import org.oxycblt.auxio.home.list.GenreListFragment
|
|||
import org.oxycblt.auxio.home.list.PlaylistListFragment
|
||||
import org.oxycblt.auxio.home.list.SongListFragment
|
||||
import org.oxycblt.auxio.home.tabs.AdaptiveTabStrategy
|
||||
import org.oxycblt.auxio.home.tabs.Tab
|
||||
import org.oxycblt.auxio.list.Sort
|
||||
import org.oxycblt.auxio.list.selection.SelectionFragment
|
||||
import org.oxycblt.auxio.list.selection.SelectionViewModel
|
||||
|
@ -152,9 +153,10 @@ class HomeFragment :
|
|||
setOnApplyWindowInsetsListener { _, insets -> insets }
|
||||
|
||||
// We know that there will only be a fixed amount of tabs, so we manually set this
|
||||
// limit to that. This also prevents the appbar lift state from being confused during
|
||||
// page transitions.
|
||||
offscreenPageLimit = homeModel.currentTabModes.size
|
||||
// limit to the maximum amount possible. This will prevent the tab ripple from
|
||||
// bugging out due to dynamically inflating each fragment, at the cost of slower
|
||||
// debug UI performance.
|
||||
offscreenPageLimit = Tab.MAX_SEQUENCE_IDX + 1
|
||||
|
||||
// By default, ViewPager2's sensitivity is high enough to result in vertical scroll
|
||||
// events being registered as horizontal scroll events. Reflect into the internal
|
||||
|
|
|
@ -59,7 +59,7 @@ sealed class Tab(open val mode: MusicMode) {
|
|||
// MusicMode for this tab.
|
||||
|
||||
/** The maximum index that a well-formed tab sequence should be. */
|
||||
private const val MAX_SEQUENCE_IDX = 4
|
||||
const val MAX_SEQUENCE_IDX = 4
|
||||
|
||||
/**
|
||||
* The default tab sequence, in integer form. This represents a set of four visible tabs
|
||||
|
|
|
@ -379,7 +379,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
* @param desc The content description to describe the bound data.
|
||||
* @param errorRes The resource of the error drawable to use if the cover cannot be loaded.
|
||||
*/
|
||||
fun bind(songs: List<Song>, desc: String, @DrawableRes errorRes: Int) {
|
||||
fun bind(songs: Collection<Song>, desc: String, @DrawableRes errorRes: Int) {
|
||||
val request =
|
||||
ImageRequest.Builder(context)
|
||||
.data(songs)
|
||||
|
|
|
@ -27,22 +27,22 @@ import javax.inject.Inject
|
|||
import org.oxycblt.auxio.music.Song
|
||||
|
||||
class SongKeyer @Inject constructor(private val coverExtractor: CoverExtractor) :
|
||||
Keyer<List<Song>> {
|
||||
override fun key(data: List<Song>, options: Options) =
|
||||
Keyer<Collection<Song>> {
|
||||
override fun key(data: Collection<Song>, options: Options) =
|
||||
"${coverExtractor.computeCoverOrdering(data).hashCode()}"
|
||||
}
|
||||
|
||||
class SongCoverFetcher
|
||||
private constructor(
|
||||
private val songs: List<Song>,
|
||||
private val songs: Collection<Song>,
|
||||
private val size: Size,
|
||||
private val coverExtractor: CoverExtractor,
|
||||
) : Fetcher {
|
||||
override suspend fun fetch() = coverExtractor.extract(songs, size)
|
||||
|
||||
class Factory @Inject constructor(private val coverExtractor: CoverExtractor) :
|
||||
Fetcher.Factory<List<Song>> {
|
||||
override fun create(data: List<Song>, options: Options, imageLoader: ImageLoader) =
|
||||
Fetcher.Factory<Collection<Song>> {
|
||||
override fun create(data: Collection<Song>, options: Options, imageLoader: ImageLoader) =
|
||||
SongCoverFetcher(data, options.size, coverExtractor)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -77,7 +77,7 @@ constructor(
|
|||
* will be returned of a mosaic composed of four album covers ordered by
|
||||
* [computeCoverOrdering]. Otherwise, a [SourceResult] of one album cover will be returned.
|
||||
*/
|
||||
suspend fun extract(songs: List<Song>, size: Size): FetchResult? {
|
||||
suspend fun extract(songs: Collection<Song>, size: Size): FetchResult? {
|
||||
val albums = computeCoverOrdering(songs)
|
||||
val streams = mutableListOf<InputStream>()
|
||||
for (album in albums) {
|
||||
|
@ -117,7 +117,7 @@ constructor(
|
|||
* by their names. "Representation" is defined by how many [Song]s were found to be linked to
|
||||
* the given [Album] in the given [Song] list.
|
||||
*/
|
||||
fun computeCoverOrdering(songs: List<Song>): List<Album> {
|
||||
fun computeCoverOrdering(songs: Collection<Song>): List<Album> {
|
||||
// TODO: Start short-circuiting in more places
|
||||
if (songs.isEmpty()) return listOf()
|
||||
if (songs.size == 1) return listOf(songs.first().album)
|
||||
|
@ -150,7 +150,7 @@ constructor(
|
|||
MediaMetadataRetriever().run {
|
||||
// This call is time-consuming but it also doesn't seem to hold up the main thread,
|
||||
// so it's probably fine not to wrap it.rmt
|
||||
setDataSource(context, album.songs[0].uri)
|
||||
setDataSource(context, album.coverUri.song)
|
||||
|
||||
// Get the embedded picture from MediaMetadataRetriever, which will return a full
|
||||
// ByteArray of the cover without any compression artifacts.
|
||||
|
@ -161,7 +161,7 @@ constructor(
|
|||
private suspend fun extractExoplayerCover(album: Album): InputStream? {
|
||||
val tracks =
|
||||
MetadataRetriever.retrieveMetadata(
|
||||
mediaSourceFactory, MediaItem.fromUri(album.songs[0].uri))
|
||||
mediaSourceFactory, MediaItem.fromUri(album.coverUri.song))
|
||||
.asDeferred()
|
||||
.await()
|
||||
|
||||
|
@ -207,7 +207,9 @@ constructor(
|
|||
|
||||
private suspend fun extractMediaStoreCover(album: Album) =
|
||||
// Eliminate any chance that this blocking call might mess up the loading process
|
||||
withContext(Dispatchers.IO) { context.contentResolver.openInputStream(album.coverUri) }
|
||||
withContext(Dispatchers.IO) {
|
||||
context.contentResolver.openInputStream(album.coverUri.mediaStore)
|
||||
}
|
||||
|
||||
/** Derived from phonograph: https://github.com/kabouzeid/Phonograph */
|
||||
private fun createMosaic(streams: List<InputStream>, size: Size): FetchResult {
|
||||
|
|
|
@ -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)
|
|
@ -56,7 +56,6 @@ constructor(
|
|||
}
|
||||
|
||||
override fun onMusicChanges(changes: MusicRepository.Changes) {
|
||||
if (!changes.deviceLibrary) return
|
||||
val deviceLibrary = musicRepository.deviceLibrary ?: return
|
||||
val userLibrary = musicRepository.userLibrary ?: return
|
||||
// Sanitize the selection to remove items that no longer exist and thus
|
||||
|
|
|
@ -27,6 +27,7 @@ import java.util.UUID
|
|||
import kotlin.math.max
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.oxycblt.auxio.image.extractor.CoverUri
|
||||
import org.oxycblt.auxio.list.Item
|
||||
import org.oxycblt.auxio.music.fs.MimeType
|
||||
import org.oxycblt.auxio.music.fs.Path
|
||||
|
@ -34,6 +35,7 @@ import org.oxycblt.auxio.music.info.Date
|
|||
import org.oxycblt.auxio.music.info.Disc
|
||||
import org.oxycblt.auxio.music.info.Name
|
||||
import org.oxycblt.auxio.music.info.ReleaseType
|
||||
import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment
|
||||
import org.oxycblt.auxio.util.concatLocalized
|
||||
import org.oxycblt.auxio.util.toUuidOrNull
|
||||
|
||||
|
@ -224,7 +226,7 @@ sealed interface Music : Item {
|
|||
*/
|
||||
sealed interface MusicParent : Music {
|
||||
/** 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
|
||||
/** The duration of the audio file, in milliseconds. */
|
||||
val durationMs: Long
|
||||
/** The ReplayGain adjustment to apply during playback. */
|
||||
val replayGainAdjustment: ReplayGainAdjustment
|
||||
/** The date the audio file was added to the device, as a unix epoch timestamp. */
|
||||
val dateAdded: Long
|
||||
/**
|
||||
|
@ -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
|
||||
* cost of image quality.
|
||||
*/
|
||||
val coverUri: Uri
|
||||
val coverUri: CoverUri
|
||||
/** The duration of all songs in the album, in milliseconds. */
|
||||
val durationMs: Long
|
||||
/** The earliest date a song in this album was added, as a unix epoch timestamp. */
|
||||
|
@ -318,14 +322,11 @@ interface Artist : MusicParent {
|
|||
* Note that any [Song] credited to this artist will have it's [Album] considered to be
|
||||
* "indirectly" linked to this [Artist], and thus included in this list.
|
||||
*/
|
||||
val albums: List<Album>
|
||||
|
||||
val albums: Collection<Album>
|
||||
/** 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. */
|
||||
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
|
||||
* songs.
|
||||
|
@ -342,7 +343,7 @@ interface Artist : MusicParent {
|
|||
*/
|
||||
interface Genre : MusicParent {
|
||||
/** 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. */
|
||||
val durationMs: Long
|
||||
}
|
||||
|
@ -353,6 +354,7 @@ interface Genre : MusicParent {
|
|||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
interface Playlist : MusicParent {
|
||||
override val songs: List<Song>
|
||||
/** The total duration of the songs in this genre, in milliseconds. */
|
||||
val durationMs: Long
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ import kotlinx.coroutines.Job
|
|||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.yield
|
||||
import org.oxycblt.auxio.music.cache.CacheRepository
|
||||
import org.oxycblt.auxio.music.device.DeviceLibrary
|
||||
|
@ -300,35 +301,35 @@ constructor(
|
|||
val userLibrary = synchronized(this) { userLibrary ?: return }
|
||||
logD("Creating playlist $name with ${songs.size} songs")
|
||||
userLibrary.createPlaylist(name, songs)
|
||||
emitLibraryChange(device = false, user = true)
|
||||
dispatchLibraryChange(device = false, user = true)
|
||||
}
|
||||
|
||||
override suspend fun renamePlaylist(playlist: Playlist, name: String) {
|
||||
val userLibrary = synchronized(this) { userLibrary ?: return }
|
||||
logD("Renaming $playlist to $name")
|
||||
userLibrary.renamePlaylist(playlist, name)
|
||||
emitLibraryChange(device = false, user = true)
|
||||
dispatchLibraryChange(device = false, user = true)
|
||||
}
|
||||
|
||||
override suspend fun deletePlaylist(playlist: Playlist) {
|
||||
val userLibrary = synchronized(this) { userLibrary ?: return }
|
||||
logD("Deleting $playlist")
|
||||
userLibrary.deletePlaylist(playlist)
|
||||
emitLibraryChange(device = false, user = true)
|
||||
dispatchLibraryChange(device = false, user = true)
|
||||
}
|
||||
|
||||
override suspend fun addToPlaylist(songs: List<Song>, playlist: Playlist) {
|
||||
val userLibrary = synchronized(this) { userLibrary ?: return }
|
||||
logD("Adding ${songs.size} songs to $playlist")
|
||||
userLibrary.addToPlaylist(playlist, songs)
|
||||
emitLibraryChange(device = false, user = true)
|
||||
dispatchLibraryChange(device = false, user = true)
|
||||
}
|
||||
|
||||
override suspend fun rewritePlaylist(playlist: Playlist, songs: List<Song>) {
|
||||
val userLibrary = synchronized(this) { userLibrary ?: return }
|
||||
logD("Rewriting $playlist with ${songs.size} songs")
|
||||
userLibrary.rewritePlaylist(playlist, songs)
|
||||
emitLibraryChange(device = false, user = true)
|
||||
dispatchLibraryChange(device = false, user = true)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
|
@ -353,7 +354,7 @@ constructor(
|
|||
// Music loading process failed due to something we have not handled.
|
||||
logE("Music indexing failed")
|
||||
logE(e.stackTraceToString())
|
||||
emitComplete(e)
|
||||
emitIndexingCompletion(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -367,7 +368,7 @@ constructor(
|
|||
|
||||
// Start initializing the extractors. Use an indeterminate state, as there is no ETA on
|
||||
// how long a media database query will take.
|
||||
emitLoading(IndexingProgress.Indeterminate)
|
||||
emitIndexingProgress(IndexingProgress.Indeterminate)
|
||||
|
||||
// Do the initial query of the cache and media databases in parallel.
|
||||
logD("Starting MediaStore query")
|
||||
|
@ -388,6 +389,7 @@ constructor(
|
|||
logD("Starting song discovery")
|
||||
val completeSongs = Channel<RawSong>(Channel.UNLIMITED)
|
||||
val incompleteSongs = Channel<RawSong>(Channel.UNLIMITED)
|
||||
val processedSongs = Channel<RawSong>(Channel.UNLIMITED)
|
||||
logD("Started MediaStore discovery")
|
||||
val mediaStoreJob =
|
||||
worker.scope.tryAsync {
|
||||
|
@ -400,12 +402,19 @@ constructor(
|
|||
tagExtractor.consume(incompleteSongs, completeSongs)
|
||||
completeSongs.close()
|
||||
}
|
||||
logD("Starting DeviceLibrary creation")
|
||||
val deviceLibraryJob =
|
||||
worker.scope.tryAsync(Dispatchers.Default) {
|
||||
deviceLibraryFactory.create(completeSongs, processedSongs).also {
|
||||
processedSongs.close()
|
||||
}
|
||||
}
|
||||
|
||||
// Await completed raw songs as they are processed.
|
||||
val rawSongs = LinkedList<RawSong>()
|
||||
for (rawSong in completeSongs) {
|
||||
for (rawSong in processedSongs) {
|
||||
rawSongs.add(rawSong)
|
||||
emitLoading(IndexingProgress.Songs(rawSongs.size, query.projectedTotal))
|
||||
emitIndexingProgress(IndexingProgress.Songs(rawSongs.size, query.projectedTotal))
|
||||
}
|
||||
logD("Awaiting discovery completion")
|
||||
// These should be no-ops, but we need the error state to see if we should keep going.
|
||||
|
@ -417,47 +426,45 @@ constructor(
|
|||
throw NoMusicException()
|
||||
}
|
||||
|
||||
// Successfully loaded the library, now save the cache, create the library, and
|
||||
// read playlist information in parallel.
|
||||
// Successfully loaded the library, now save the cache and read playlist information
|
||||
// in parallel.
|
||||
logD("Discovered ${rawSongs.size} songs, starting finalization")
|
||||
// TODO: Indicate playlist state in loading process?
|
||||
emitLoading(IndexingProgress.Indeterminate)
|
||||
val deviceLibraryChannel = Channel<DeviceLibrary>()
|
||||
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() }
|
||||
}
|
||||
emitIndexingProgress(IndexingProgress.Indeterminate)
|
||||
logD("Starting UserLibrary query")
|
||||
val userLibraryQueryJob = worker.scope.tryAsync { userLibraryFactory.query() }
|
||||
if (cache == null || cache.invalidated) {
|
||||
logD("Writing cache [why=${cache?.invalidated}]")
|
||||
cacheRepository.writeCache(rawSongs)
|
||||
}
|
||||
logD("Awaiting library creation")
|
||||
logD("Awaiting UserLibrary query")
|
||||
val rawPlaylists = userLibraryQueryJob.await().getOrThrow()
|
||||
logD("Awaiting DeviceLibrary creation")
|
||||
val deviceLibrary = deviceLibraryJob.await().getOrThrow()
|
||||
val userLibrary = userLibraryJob.await().getOrThrow()
|
||||
logD("Starting UserLibrary creation")
|
||||
val userLibrary = userLibraryFactory.create(rawPlaylists, deviceLibrary)
|
||||
|
||||
logD("Successfully indexed music library [device=$deviceLibrary user=$userLibrary]")
|
||||
emitComplete(null)
|
||||
emitIndexingCompletion(null)
|
||||
|
||||
// Comparing the library instances is obscenely expensive, do it within the library
|
||||
val deviceLibraryChanged = this.deviceLibrary != deviceLibrary
|
||||
val userLibraryChanged = this.userLibrary != userLibrary
|
||||
if (!deviceLibraryChanged && !userLibraryChanged) {
|
||||
logD("Library has not changed, skipping update")
|
||||
return
|
||||
}
|
||||
|
||||
val deviceLibraryChanged: Boolean
|
||||
val userLibraryChanged: Boolean
|
||||
synchronized(this) {
|
||||
deviceLibraryChanged = this.deviceLibrary != deviceLibrary
|
||||
userLibraryChanged = this.userLibrary != userLibrary
|
||||
if (!deviceLibraryChanged && !userLibraryChanged) {
|
||||
logD("Library has not changed, skipping update")
|
||||
return
|
||||
}
|
||||
|
||||
this.deviceLibrary = deviceLibrary
|
||||
this.userLibrary = userLibrary
|
||||
}
|
||||
|
||||
emitLibraryChange(deviceLibraryChanged, userLibraryChanged)
|
||||
withContext(Dispatchers.Main) {
|
||||
dispatchLibraryChange(deviceLibraryChanged, userLibraryChanged)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -476,7 +483,7 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun emitLoading(progress: IndexingProgress) {
|
||||
private suspend fun emitIndexingProgress(progress: IndexingProgress) {
|
||||
yield()
|
||||
synchronized(this) {
|
||||
currentIndexingState = IndexingState.Indexing(progress)
|
||||
|
@ -486,7 +493,7 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun emitComplete(error: Exception?) {
|
||||
private suspend fun emitIndexingCompletion(error: Exception?) {
|
||||
yield()
|
||||
synchronized(this) {
|
||||
previousCompletedState = IndexingState.Completed(error)
|
||||
|
@ -499,7 +506,7 @@ constructor(
|
|||
}
|
||||
|
||||
@Synchronized
|
||||
private fun emitLibraryChange(device: Boolean, user: Boolean) {
|
||||
private fun dispatchLibraryChange(device: Boolean, user: Boolean) {
|
||||
val changes = MusicRepository.Changes(device, user)
|
||||
logD("Dispatching library change [changes=$changes]")
|
||||
for (listener in updateListeners) {
|
||||
|
|
|
@ -32,19 +32,19 @@ import org.oxycblt.auxio.music.info.Date
|
|||
import org.oxycblt.auxio.music.metadata.correctWhitespace
|
||||
import org.oxycblt.auxio.music.metadata.splitEscaped
|
||||
|
||||
@Database(entities = [CachedSong::class], version = 27, exportSchema = false)
|
||||
@Database(entities = [CachedSong::class], version = 32, exportSchema = false)
|
||||
abstract class CacheDatabase : RoomDatabase() {
|
||||
abstract fun cachedSongsDao(): CachedSongsDao
|
||||
}
|
||||
|
||||
@Dao
|
||||
interface CachedSongsDao {
|
||||
@Query("SELECT * FROM ${CachedSong.TABLE_NAME}") suspend fun readSongs(): List<CachedSong>
|
||||
@Query("DELETE FROM ${CachedSong.TABLE_NAME}") suspend fun nukeSongs()
|
||||
@Query("SELECT * FROM CachedSong") suspend fun readSongs(): List<CachedSong>
|
||||
@Query("DELETE FROM CachedSong") suspend fun nukeSongs()
|
||||
@Insert suspend fun insertSongs(songs: List<CachedSong>)
|
||||
}
|
||||
|
||||
@Entity(tableName = CachedSong.TABLE_NAME)
|
||||
@Entity
|
||||
@TypeConverters(CachedSong.Converters::class)
|
||||
data class CachedSong(
|
||||
/**
|
||||
|
@ -60,6 +60,10 @@ data class CachedSong(
|
|||
var size: Long? = null,
|
||||
/** @see RawSong */
|
||||
var durationMs: Long,
|
||||
/** @see RawSong.replayGainTrackAdjustment */
|
||||
val replayGainTrackAdjustment: Float?,
|
||||
/** @see RawSong.replayGainAlbumAdjustment */
|
||||
val replayGainAlbumAdjustment: Float?,
|
||||
/** @see RawSong.musicBrainzId */
|
||||
var musicBrainzId: String? = null,
|
||||
/** @see RawSong.name */
|
||||
|
@ -97,7 +101,7 @@ data class CachedSong(
|
|||
/** @see RawSong.genreNames */
|
||||
var genreNames: List<String> = listOf()
|
||||
) {
|
||||
fun copyToRaw(rawSong: RawSong): CachedSong {
|
||||
fun copyToRaw(rawSong: RawSong) {
|
||||
rawSong.musicBrainzId = musicBrainzId
|
||||
rawSong.name = name
|
||||
rawSong.sortName = sortName
|
||||
|
@ -105,6 +109,9 @@ data class CachedSong(
|
|||
rawSong.size = size
|
||||
rawSong.durationMs = durationMs
|
||||
|
||||
rawSong.replayGainTrackAdjustment = replayGainTrackAdjustment
|
||||
rawSong.replayGainAlbumAdjustment = replayGainAlbumAdjustment
|
||||
|
||||
rawSong.track = track
|
||||
rawSong.disc = disc
|
||||
rawSong.subtitle = subtitle
|
||||
|
@ -124,7 +131,6 @@ data class CachedSong(
|
|||
rawSong.albumArtistSortNames = albumArtistSortNames
|
||||
|
||||
rawSong.genreNames = genreNames
|
||||
return this
|
||||
}
|
||||
|
||||
object Converters {
|
||||
|
@ -141,8 +147,6 @@ data class CachedSong(
|
|||
}
|
||||
|
||||
companion object {
|
||||
const val TABLE_NAME = "cached_songs"
|
||||
|
||||
fun fromRaw(rawSong: RawSong) =
|
||||
CachedSong(
|
||||
mediaStoreId =
|
||||
|
@ -155,6 +159,8 @@ data class CachedSong(
|
|||
sortName = rawSong.sortName,
|
||||
size = rawSong.size,
|
||||
durationMs = requireNotNull(rawSong.durationMs) { "Invalid raw: No duration" },
|
||||
replayGainTrackAdjustment = rawSong.replayGainTrackAdjustment,
|
||||
replayGainAlbumAdjustment = rawSong.replayGainAlbumAdjustment,
|
||||
track = rawSong.track,
|
||||
disc = rawSong.disc,
|
||||
subtitle = rawSong.subtitle,
|
||||
|
|
|
@ -43,8 +43,6 @@ class CacheRoomModule {
|
|||
Room.databaseBuilder(
|
||||
context.applicationContext, CacheDatabase::class.java, "music_cache.db")
|
||||
.fallbackToDestructiveMigration()
|
||||
.fallbackToDestructiveMigrationFrom(0)
|
||||
.fallbackToDestructiveMigrationOnDowngrade()
|
||||
.build()
|
||||
|
||||
@Provides fun cachedSongsDao(database: CacheDatabase) = database.cachedSongsDao()
|
||||
|
|
|
@ -22,7 +22,7 @@ import android.content.Context
|
|||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
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.Artist
|
||||
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.fs.contentResolverSafe
|
||||
import org.oxycblt.auxio.music.fs.useQuery
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.logW
|
||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||
|
||||
/**
|
||||
* Organized music library information obtained from device storage.
|
||||
|
@ -45,13 +46,13 @@ import org.oxycblt.auxio.util.logD
|
|||
*/
|
||||
interface DeviceLibrary {
|
||||
/** All [Song]s in this [DeviceLibrary]. */
|
||||
val songs: List<Song>
|
||||
val songs: Collection<Song>
|
||||
/** All [Album]s in this [DeviceLibrary]. */
|
||||
val albums: List<Album>
|
||||
val albums: Collection<Album>
|
||||
/** All [Artist]s in this [DeviceLibrary]. */
|
||||
val artists: List<Artist>
|
||||
val artists: Collection<Artist>
|
||||
/** All [Genre]s in this [DeviceLibrary]. */
|
||||
val genres: List<Genre>
|
||||
val genres: Collection<Genre>
|
||||
|
||||
/**
|
||||
* Find a [Song] instance corresponding to the given [Music.UID].
|
||||
|
@ -97,37 +98,166 @@ interface DeviceLibrary {
|
|||
/** Constructs a [DeviceLibrary] implementation in an asynchronous manner. */
|
||||
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
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Create an instance of [DeviceLibrary].
|
||||
*
|
||||
* @param rawSongs [RawSong]s to create the library out of.
|
||||
* @param settings [MusicSettings] required.
|
||||
*/
|
||||
fun from(rawSongs: List<RawSong>, settings: MusicSettings): DeviceLibrary =
|
||||
DeviceLibraryImpl(rawSongs, settings)
|
||||
suspend fun create(
|
||||
rawSongs: Channel<RawSong>,
|
||||
processedSongs: Channel<RawSong>
|
||||
): DeviceLibraryImpl
|
||||
}
|
||||
}
|
||||
|
||||
class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: MusicSettings) :
|
||||
DeviceLibrary.Factory {
|
||||
override suspend fun create(rawSongs: List<RawSong>): DeviceLibrary =
|
||||
DeviceLibraryImpl(rawSongs, musicSettings)
|
||||
override suspend fun create(
|
||||
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 {
|
||||
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)
|
||||
// TODO: Avoid redundant data creation
|
||||
|
||||
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.
|
||||
private val songUidMap = buildMap { songs.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 hashCode() = songs.hashCode()
|
||||
override fun toString() =
|
||||
"DeviceLibrary(songs=${songs.size}, albums=${albums.size}, artists=${artists.size}, genres=${genres.size})"
|
||||
"DeviceLibrary(songs=${songs.size}, albums=${albums.size}, " +
|
||||
"artists=${artists.size}, genres=${genres.size})"
|
||||
|
||||
override fun findSong(uid: Music.UID) = songUidMap[uid]
|
||||
override fun findAlbum(uid: Music.UID) = albumUidMap[uid]
|
||||
override fun findArtist(uid: Music.UID) = artistUidMap[uid]
|
||||
override fun findGenre(uid: Music.UID) = genreUidMap[uid]
|
||||
override fun findSong(uid: Music.UID): Song? = songUidMap[uid]
|
||||
override fun findAlbum(uid: Music.UID): Album? = albumUidMap[uid]
|
||||
override fun findArtist(uid: Music.UID): Artist? = artistUidMap[uid]
|
||||
override fun findGenre(uid: Music.UID): Genre? = genreUidMap[uid]
|
||||
|
||||
override fun findSongForUri(context: Context, uri: Uri) =
|
||||
context.contentResolverSafe.useQuery(
|
||||
|
@ -156,70 +287,4 @@ private class DeviceLibraryImpl(rawSongs: List<RawSong>, settings: MusicSettings
|
|||
val size = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
package org.oxycblt.auxio.music.device
|
||||
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.image.extractor.CoverUri
|
||||
import org.oxycblt.auxio.list.Sort
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
|
@ -37,6 +38,7 @@ import org.oxycblt.auxio.music.info.Name
|
|||
import org.oxycblt.auxio.music.info.ReleaseType
|
||||
import org.oxycblt.auxio.music.metadata.parseId3GenreNames
|
||||
import org.oxycblt.auxio.music.metadata.parseMultiValue
|
||||
import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment
|
||||
import org.oxycblt.auxio.util.nonZeroOrNull
|
||||
import org.oxycblt.auxio.util.toUuidOrNull
|
||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||
|
@ -88,6 +90,10 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son
|
|||
fromFormat = null)
|
||||
override val size = requireNotNull(rawSong.size) { "Invalid raw: No size" }
|
||||
override val durationMs = requireNotNull(rawSong.durationMs) { "Invalid raw: No duration" }
|
||||
override val replayGainAdjustment =
|
||||
ReplayGainAdjustment(
|
||||
track = rawSong.replayGainTrackAdjustment, album = rawSong.replayGainAlbumAdjustment)
|
||||
|
||||
override val dateAdded = requireNotNull(rawSong.dateAdded) { "Invalid raw: No date added" }
|
||||
private var _album: AlbumImpl? = null
|
||||
override val album: Album
|
||||
|
@ -226,17 +232,16 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son
|
|||
/**
|
||||
* Library-backed implementation of [Album].
|
||||
*
|
||||
* @param rawAlbum The [RawAlbum] to derive the member data from.
|
||||
* @param grouping [Grouping] to derive the member data from.
|
||||
* @param musicSettings [MusicSettings] to for user parsing configuration.
|
||||
* @param songs The [Song]s that are a part of this [Album]. These items will be linked to this
|
||||
* [Album].
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class AlbumImpl(
|
||||
private val rawAlbum: RawAlbum,
|
||||
grouping: Grouping<RawAlbum, SongImpl>,
|
||||
musicSettings: MusicSettings,
|
||||
override val songs: List<SongImpl>
|
||||
) : Album {
|
||||
private val rawAlbum = grouping.raw.inner
|
||||
|
||||
override val uid =
|
||||
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
|
||||
rawAlbum.musicBrainzId?.let { Music.UID.musicBrainz(MusicMode.ALBUMS, it) }
|
||||
|
@ -250,7 +255,7 @@ class AlbumImpl(
|
|||
override val name = Name.Known.from(rawAlbum.name, rawAlbum.sortName, musicSettings)
|
||||
override val dates: Date.Range?
|
||||
override val releaseType = rawAlbum.releaseType ?: ReleaseType.Album(null)
|
||||
override val coverUri = rawAlbum.mediaStoreId.toCoverUri()
|
||||
override val coverUri = CoverUri(rawAlbum.mediaStoreId.toCoverUri(), grouping.raw.src.uri)
|
||||
override val durationMs: Long
|
||||
override val dateAdded: Long
|
||||
|
||||
|
@ -258,6 +263,8 @@ class AlbumImpl(
|
|||
override val artists: List<Artist>
|
||||
get() = _artists
|
||||
|
||||
override val songs: Set<Song> = grouping.music
|
||||
|
||||
private var hashCode = uid.hashCode()
|
||||
|
||||
init {
|
||||
|
@ -267,7 +274,7 @@ class AlbumImpl(
|
|||
var earliestDateAdded: Long = Long.MAX_VALUE
|
||||
|
||||
// Do linking and value generation in the same loop for efficiency.
|
||||
for (song in songs) {
|
||||
for (song in grouping.music) {
|
||||
song.link(this)
|
||||
|
||||
if (song.date != null) {
|
||||
|
@ -342,18 +349,13 @@ class AlbumImpl(
|
|||
/**
|
||||
* Library-backed implementation of [Artist].
|
||||
*
|
||||
* @param rawArtist The [RawArtist] to derive the member data from.
|
||||
* @param grouping [Grouping] to derive the member data from.
|
||||
* @param musicSettings [MusicSettings] to for user parsing configuration.
|
||||
* @param songAlbums A list of the [Song]s and [Album]s that are a part of this [Artist] , either
|
||||
* through artist or album artist tags. Providing [Song]s to the artist is optional. These
|
||||
* instances will be linked to this [Artist].
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class ArtistImpl(
|
||||
private val rawArtist: RawArtist,
|
||||
musicSettings: MusicSettings,
|
||||
songAlbums: List<Music>
|
||||
) : Artist {
|
||||
class ArtistImpl(grouping: Grouping<RawArtist, Music>, musicSettings: MusicSettings) : Artist {
|
||||
private val rawArtist = grouping.raw.inner
|
||||
|
||||
override val uid =
|
||||
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
|
||||
rawArtist.musicBrainzId?.let { Music.UID.musicBrainz(MusicMode.ARTISTS, it) }
|
||||
|
@ -362,10 +364,10 @@ class ArtistImpl(
|
|||
rawArtist.name?.let { Name.Known.from(it, rawArtist.sortName, musicSettings) }
|
||||
?: Name.Unknown(R.string.def_artist)
|
||||
|
||||
override val songs: List<Song>
|
||||
override val albums: List<Album>
|
||||
override val explicitAlbums: List<Album>
|
||||
override val implicitAlbums: List<Album>
|
||||
override val songs: Set<Song>
|
||||
override val albums: Set<Album>
|
||||
override val explicitAlbums: Set<Album>
|
||||
override val implicitAlbums: Set<Album>
|
||||
override val durationMs: Long?
|
||||
|
||||
override lateinit var genres: List<Genre>
|
||||
|
@ -376,7 +378,7 @@ class ArtistImpl(
|
|||
val distinctSongs = mutableSetOf<Song>()
|
||||
val albumMap = mutableMapOf<Album, Boolean>()
|
||||
|
||||
for (music in songAlbums) {
|
||||
for (music in grouping.music) {
|
||||
when (music) {
|
||||
is SongImpl -> {
|
||||
music.link(this)
|
||||
|
@ -393,10 +395,10 @@ class ArtistImpl(
|
|||
}
|
||||
}
|
||||
|
||||
songs = distinctSongs.toList()
|
||||
albums = Sort(Sort.Mode.ByDate, Sort.Direction.DESCENDING).albums(albumMap.keys)
|
||||
explicitAlbums = albums.filter { unlikelyToBeNull(albumMap[it]) }
|
||||
implicitAlbums = albums.filterNot { unlikelyToBeNull(albumMap[it]) }
|
||||
songs = distinctSongs
|
||||
albums = albumMap.keys
|
||||
explicitAlbums = albums.filterTo(mutableSetOf()) { albumMap[it] == true }
|
||||
implicitAlbums = albums.filterNotTo(mutableSetOf()) { albumMap[it] == true }
|
||||
durationMs = songs.sumOf { it.durationMs }.nonZeroOrNull()
|
||||
|
||||
hashCode = 31 * hashCode + rawArtist.hashCode()
|
||||
|
@ -444,40 +446,38 @@ class ArtistImpl(
|
|||
/**
|
||||
* Library-backed implementation of [Genre].
|
||||
*
|
||||
* @param rawGenre [RawGenre] to derive the member data from.
|
||||
* @param grouping [Grouping] to derive the member data from.
|
||||
* @param musicSettings [MusicSettings] to for user parsing configuration.
|
||||
* @param songs Child [SongImpl]s of this instance.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class GenreImpl(
|
||||
private val rawGenre: RawGenre,
|
||||
musicSettings: MusicSettings,
|
||||
override val songs: List<SongImpl>
|
||||
) : Genre {
|
||||
class GenreImpl(grouping: Grouping<RawGenre, SongImpl>, musicSettings: MusicSettings) : Genre {
|
||||
private val rawGenre = grouping.raw.inner
|
||||
|
||||
override val uid = Music.UID.auxio(MusicMode.GENRES) { update(rawGenre.name) }
|
||||
override val name =
|
||||
rawGenre.name?.let { Name.Known.from(it, rawGenre.name, musicSettings) }
|
||||
?: Name.Unknown(R.string.def_genre)
|
||||
|
||||
override val artists: List<Artist>
|
||||
override val songs: Set<Song>
|
||||
override val artists: Set<Artist>
|
||||
override val durationMs: Long
|
||||
|
||||
private var hashCode = uid.hashCode()
|
||||
|
||||
init {
|
||||
val distinctAlbums = mutableSetOf<Album>()
|
||||
val distinctArtists = mutableSetOf<Artist>()
|
||||
var totalDuration = 0L
|
||||
|
||||
for (song in songs) {
|
||||
for (song in grouping.music) {
|
||||
song.link(this)
|
||||
distinctAlbums.add(song.album)
|
||||
distinctArtists.addAll(song.artists)
|
||||
totalDuration += song.durationMs
|
||||
}
|
||||
|
||||
artists = Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING).artists(distinctArtists)
|
||||
songs = grouping.music
|
||||
artists = distinctArtists
|
||||
durationMs = totalDuration
|
||||
|
||||
hashCode = 31 * hashCode + rawGenre.hashCode()
|
||||
hashCode = 31 * hashCode + songs.hashCode()
|
||||
}
|
||||
|
|
|
@ -51,6 +51,10 @@ data class RawSong(
|
|||
var durationMs: Long? = null,
|
||||
/** @see Song.mimeType */
|
||||
var extensionMimeType: String? = null,
|
||||
/** @see Song.replayGainAdjustment */
|
||||
var replayGainTrackAdjustment: Float? = null,
|
||||
/** @see Song.replayGainAdjustment */
|
||||
var replayGainAlbumAdjustment: Float? = null,
|
||||
/** @see Music.UID */
|
||||
var musicBrainzId: String? = null,
|
||||
/** @see Music.name */
|
||||
|
@ -115,30 +119,33 @@ data class RawAlbum(
|
|||
) {
|
||||
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:
|
||||
// - 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.
|
||||
// - If we do not have a MusicBrainz ID, compare by the lowercase album name and lowercase
|
||||
// artist name. This allows for case-insensitive artist/album grouping, which can be common
|
||||
// for albums/artists that have different naming (ex. "RAMMSTEIN" vs. "Rammstein").
|
||||
private val artistKeys = inner.rawArtists.map { it.key }
|
||||
|
||||
// Cache the hash-code for HashMap efficiency.
|
||||
private val hashCode =
|
||||
value.musicBrainzId?.hashCode()
|
||||
?: (31 * value.name.lowercase().hashCode() + value.rawArtists.hashCode())
|
||||
inner.musicBrainzId?.hashCode()
|
||||
?: (31 * inner.name.lowercase().hashCode() + artistKeys.hashCode())
|
||||
|
||||
override fun hashCode() = hashCode
|
||||
|
||||
override fun equals(other: Any?) =
|
||||
other is Key &&
|
||||
when {
|
||||
value.musicBrainzId != null && other.value.musicBrainzId != null ->
|
||||
value.musicBrainzId == other.value.musicBrainzId
|
||||
value.musicBrainzId == null && other.value.musicBrainzId == null ->
|
||||
other.value.name.equals(other.value.name, true) &&
|
||||
other.value.rawArtists == other.value.rawArtists
|
||||
inner.musicBrainzId != null && other.inner.musicBrainzId != null ->
|
||||
inner.musicBrainzId == other.inner.musicBrainzId
|
||||
inner.musicBrainzId == null && other.inner.musicBrainzId == null ->
|
||||
inner.name.equals(other.inner.name, true) && artistKeys == other.artistKeys
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
@ -164,7 +171,7 @@ data class RawArtist(
|
|||
* Allows [RawArtist]s to be compared by "fundamental" information that is unlikely to change on
|
||||
* an item-by-item
|
||||
*/
|
||||
data class Key(val value: RawArtist) {
|
||||
data class Key(private val inner: RawArtist) {
|
||||
// Artists are grouped as follows:
|
||||
// - If we have a MusicBrainz ID, only group by it. This allows different Artists with the
|
||||
// same name to be differentiated, which is common in large libraries.
|
||||
|
@ -172,7 +179,7 @@ data class RawArtist(
|
|||
// grouping to be case-insensitive.
|
||||
|
||||
// Cache the hashCode for HashMap efficiency.
|
||||
private val hashCode = value.musicBrainzId?.hashCode() ?: value.name?.lowercase().hashCode()
|
||||
val hashCode = inner.musicBrainzId?.hashCode() ?: inner.name?.lowercase().hashCode()
|
||||
|
||||
// Compare names and MusicBrainz IDs in order to differentiate artists with the
|
||||
// same name in large libraries.
|
||||
|
@ -182,13 +189,13 @@ data class RawArtist(
|
|||
override fun equals(other: Any?) =
|
||||
other is Key &&
|
||||
when {
|
||||
value.musicBrainzId != null && other.value.musicBrainzId != null ->
|
||||
value.musicBrainzId == other.value.musicBrainzId
|
||||
value.musicBrainzId == null && other.value.musicBrainzId == null ->
|
||||
inner.musicBrainzId != null && other.inner.musicBrainzId != null ->
|
||||
inner.musicBrainzId == other.inner.musicBrainzId
|
||||
inner.musicBrainzId == null && other.inner.musicBrainzId == null ->
|
||||
when {
|
||||
value.name != null && other.value.name != null ->
|
||||
value.name.equals(other.value.name, true)
|
||||
value.name == null && other.value.name == null -> true
|
||||
inner.name != null && other.inner.name != null ->
|
||||
inner.name.equals(other.inner.name, true)
|
||||
inner.name == null && other.inner.name == null -> true
|
||||
else -> false
|
||||
}
|
||||
else -> false
|
||||
|
@ -207,9 +214,13 @@ data class RawGenre(
|
|||
) {
|
||||
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.
|
||||
private val hashCode = value.name?.lowercase().hashCode()
|
||||
private val hashCode = inner.name?.lowercase().hashCode()
|
||||
|
||||
// Only group by the lowercase genre name. This allows Genre grouping to be
|
||||
// case-insensitive, which may be helpful in some libraries with different ways of
|
||||
|
@ -219,10 +230,28 @@ data class RawGenre(
|
|||
override fun equals(other: Any?) =
|
||||
other is Key &&
|
||||
when {
|
||||
value.name != null && other.value.name != null ->
|
||||
value.name.equals(other.value.name, true)
|
||||
value.name == null && other.value.name == null -> true
|
||||
inner.name != null && other.inner.name != null ->
|
||||
inner.name.equals(other.inner.name, true)
|
||||
inner.name == null && other.inner.name == null -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
|
|
|
@ -203,8 +203,8 @@ private data class IntelligentKnownName(override val raw: String, override val s
|
|||
// Separate each token into their numeric and lexicographic counterparts.
|
||||
if (token.first().isDigit()) {
|
||||
// The digit string comparison breaks with preceding zero digits, remove those
|
||||
// TODO: Handle zero digits in other languages
|
||||
val digits = token.trimStart('0').ifEmpty { token }
|
||||
val digits =
|
||||
token.trimStart { Character.getNumericValue(it) == 0 }.ifEmpty { token }
|
||||
// Other languages have other types of digit strings, still use collation keys
|
||||
collationKey = COLLATOR.getCollationKey(digits)
|
||||
type = SortToken.Type.NUMERIC
|
||||
|
|
|
@ -30,6 +30,7 @@ import org.oxycblt.auxio.music.fs.toAudioUri
|
|||
import org.oxycblt.auxio.music.info.Date
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.logW
|
||||
import org.oxycblt.auxio.util.nonZeroOrNull
|
||||
|
||||
/**
|
||||
* An processing abstraction over the [MetadataRetriever] and [TextTags] workflow that operates on
|
||||
|
@ -140,15 +141,16 @@ private class TagWorkerImpl(
|
|||
textFrames["TALB"]?.let { rawSong.albumName = it.first() }
|
||||
textFrames["TSOA"]?.let { rawSong.albumSortName = it.first() }
|
||||
(textFrames["TXXX:musicbrainz album type"]
|
||||
?: textFrames["TXXX:releasetype"] ?: textFrames["GRP1"])
|
||||
?: textFrames["TXXX:releasetype"] ?:
|
||||
// This is a non-standard iTunes extension
|
||||
textFrames["GRP1"])
|
||||
?.let { rawSong.releaseTypes = it }
|
||||
|
||||
// Artist
|
||||
textFrames["TXXX:musicbrainz artist id"]?.let { rawSong.artistMusicBrainzIds = it }
|
||||
(textFrames["TXXX:artists"] ?: textFrames["TPE1"])?.let { rawSong.artistNames = it }
|
||||
(textFrames["TXXX:artists_sort"] ?: textFrames["TSOP"])?.let {
|
||||
rawSong.artistSortNames = it
|
||||
}
|
||||
(textFrames["TXXX:artistssort"] ?: textFrames["TXXX:artists_sort"] ?: textFrames["TSOP"])
|
||||
?.let { rawSong.artistSortNames = it }
|
||||
|
||||
// Album artist
|
||||
textFrames["TXXX:musicbrainz album artist id"]?.let {
|
||||
|
@ -157,16 +159,18 @@ private class TagWorkerImpl(
|
|||
(textFrames["TXXX:albumartists"] ?: textFrames["TPE2"])?.let {
|
||||
rawSong.albumArtistNames = it
|
||||
}
|
||||
(textFrames["TXXX:albumartists_sort"] ?: textFrames["TSO2"])?.let {
|
||||
rawSong.albumArtistSortNames = it
|
||||
}
|
||||
(textFrames["TXXX:albumartistssort"]
|
||||
?: textFrames["TXXX:albumartists_sort"] ?: textFrames["TXXX:albumartistsort"]
|
||||
// This is a non-standard iTunes extension
|
||||
?: textFrames["TSO2"])
|
||||
?.let { rawSong.albumArtistSortNames = it }
|
||||
|
||||
// Genre
|
||||
textFrames["TCON"]?.let { rawSong.genreNames = it }
|
||||
|
||||
// Compilation Flag
|
||||
(textFrames["TCMP"]
|
||||
?: textFrames["TXXX:compilation"] ?: textFrames["TXXX:itunescompilation"])
|
||||
(textFrames["TCMP"] // This is a non-standard itunes extension
|
||||
?: textFrames["TXXX:compilation"] ?: textFrames["TXXX:itunescompilation"])
|
||||
?.let {
|
||||
// Ignore invalid instances of this tag
|
||||
if (it.size != 1 || it[0] != "1") return@let
|
||||
|
@ -175,6 +179,14 @@ private class TagWorkerImpl(
|
|||
rawSong.albumArtistNames.ifEmpty { COMPILATION_ALBUM_ARTISTS }
|
||||
rawSong.releaseTypes = rawSong.releaseTypes.ifEmpty { COMPILATION_RELEASE_TYPES }
|
||||
}
|
||||
|
||||
// ReplayGain information
|
||||
textFrames["TXXX:replaygain_track_gain"]?.parseReplayGainAdjustment()?.let {
|
||||
rawSong.replayGainTrackAdjustment = it
|
||||
}
|
||||
textFrames["TXXX:replaygain_album_gain"]?.parseReplayGainAdjustment()?.let {
|
||||
rawSong.replayGainAlbumAdjustment = it
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseId3v23Date(textFrames: Map<String, List<String>>): Date? {
|
||||
|
@ -249,14 +261,18 @@ private class TagWorkerImpl(
|
|||
// Artist
|
||||
comments["musicbrainz_artistid"]?.let { rawSong.artistMusicBrainzIds = it }
|
||||
(comments["artists"] ?: comments["artist"])?.let { rawSong.artistNames = it }
|
||||
(comments["artists_sort"] ?: comments["artistsort"])?.let { rawSong.artistSortNames = it }
|
||||
(comments["artistssort"] ?: comments["artists_sort"] ?: comments["artistsort"])?.let {
|
||||
rawSong.artistSortNames = it
|
||||
}
|
||||
|
||||
// Album artist
|
||||
comments["musicbrainz_albumartistid"]?.let { rawSong.albumArtistMusicBrainzIds = it }
|
||||
(comments["albumartists"] ?: comments["albumartist"])?.let { rawSong.albumArtistNames = it }
|
||||
(comments["albumartists_sort"] ?: comments["albumartistsort"])?.let {
|
||||
rawSong.albumArtistSortNames = it
|
||||
(comments["albumartists"] ?: comments["album_artists"] ?: comments["albumartist"])?.let {
|
||||
rawSong.albumArtistNames = it
|
||||
}
|
||||
(comments["albumartistssort"]
|
||||
?: comments["albumartists_sort"] ?: comments["albumartistsort"])
|
||||
?.let { rawSong.albumArtistSortNames = it }
|
||||
|
||||
// Genre
|
||||
comments["genre"]?.let { rawSong.genreNames = it }
|
||||
|
@ -270,10 +286,38 @@ private class TagWorkerImpl(
|
|||
rawSong.albumArtistNames.ifEmpty { COMPILATION_ALBUM_ARTISTS }
|
||||
rawSong.releaseTypes = rawSong.releaseTypes.ifEmpty { COMPILATION_RELEASE_TYPES }
|
||||
}
|
||||
|
||||
// ReplayGain information
|
||||
// Most ReplayGain tags are formatted as a simple decibel adjustment in a custom
|
||||
// replaygain_*_gain tag, but opus has it's own "r128_*_gain" ReplayGain specification,
|
||||
// which requires dividing the adjustment by 256 to get the gain. This is used alongside
|
||||
// the base adjustment intrinsic to the format to create the normalized adjustment. This is
|
||||
// normally the only tag used for opus files, but some software still writes replay gain
|
||||
// tags anyway.
|
||||
(comments["r128_track_gain"]?.parseReplayGainAdjustment()?.div(256)
|
||||
?: comments["replaygain_track_gain"]?.parseReplayGainAdjustment())
|
||||
?.let { rawSong.replayGainTrackAdjustment = it }
|
||||
(comments["r128_album_gain"]?.parseReplayGainAdjustment()?.div(256)
|
||||
?: comments["replaygain_album_gain"]?.parseReplayGainAdjustment())
|
||||
?.let { rawSong.replayGainAlbumAdjustment = it }
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a ReplayGain adjustment into a float value.
|
||||
*
|
||||
* @return A parsed adjustment float, or null if the adjustment had invalid formatting.
|
||||
*/
|
||||
private fun List<String>.parseReplayGainAdjustment() =
|
||||
first().replace(REPLAYGAIN_ADJUSTMENT_FILTER_REGEX, "").toFloatOrNull()?.nonZeroOrNull()
|
||||
|
||||
private companion object {
|
||||
val COMPILATION_ALBUM_ARTISTS = listOf("Various Artists")
|
||||
val COMPILATION_RELEASE_TYPES = listOf("compilation")
|
||||
|
||||
/**
|
||||
* Matches non-float information from ReplayGain adjustments. Derived from vanilla music:
|
||||
* https://github.com/vanilla-music/vanilla
|
||||
*/
|
||||
val REPLAYGAIN_ADJUSTMENT_FILTER_REGEX by lazy { Regex("[^\\d.-]") }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,6 +28,8 @@ import androidx.media3.extractor.metadata.vorbis.VorbisComment
|
|||
*
|
||||
* @param metadata The [Metadata] to wrap.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*
|
||||
* TODO: Merge with TagWorker
|
||||
*/
|
||||
class TextTags(metadata: Metadata) {
|
||||
private val _id3v2 = mutableMapOf<String, List<String>>()
|
||||
|
|
|
@ -20,7 +20,6 @@ package org.oxycblt.auxio.music.user
|
|||
|
||||
import java.lang.Exception
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.MusicRepository
|
||||
import org.oxycblt.auxio.music.MusicSettings
|
||||
|
@ -38,10 +37,12 @@ import org.oxycblt.auxio.util.logE
|
|||
* generally not expected to create this yourself, and instead rely on MusicRepository.
|
||||
*
|
||||
* @author Alexander Capehart
|
||||
*
|
||||
* TODO: Communicate errors
|
||||
*/
|
||||
interface UserLibrary {
|
||||
/** The current user-defined playlists. */
|
||||
val playlists: List<Playlist>
|
||||
val playlists: Collection<Playlist>
|
||||
|
||||
/**
|
||||
* Find a [Playlist] instance corresponding to the given [Music.UID].
|
||||
|
@ -62,14 +63,25 @@ interface UserLibrary {
|
|||
/** Constructs a [UserLibrary] implementation in an asynchronous manner. */
|
||||
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
|
||||
* later. This allows database information to be read before the actual instance is
|
||||
* constructed.
|
||||
* @return A new [MutableUserLibrary] with the required implementation.
|
||||
* @return A list of [RawPlaylist]s.
|
||||
*/
|
||||
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 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].
|
||||
*
|
||||
* @param playlist The [Playlist] to rename.
|
||||
* @param name The name of the new [Playlist].
|
||||
* @return True if the [Playlist] was successfully renamed, false otherwise.
|
||||
*/
|
||||
suspend fun renamePlaylist(playlist: Playlist, name: String)
|
||||
suspend fun renamePlaylist(playlist: Playlist, name: String): Boolean
|
||||
|
||||
/**
|
||||
* Delete a [Playlist].
|
||||
*
|
||||
* @param playlist The playlist to delete.
|
||||
* @return True if the [Playlist] was successfully deleted, false otherwise.
|
||||
*/
|
||||
suspend fun deletePlaylist(playlist: Playlist)
|
||||
suspend fun deletePlaylist(playlist: Playlist): Boolean
|
||||
|
||||
/**
|
||||
* Add [Song]s to a [Playlist].
|
||||
*
|
||||
* @param playlist The [Playlist] to add to. Must currently exist.
|
||||
* @param songs The [Song]s to add to the [Playlist].
|
||||
* @return True if the [Song]s were successfully added, false otherwise.
|
||||
*/
|
||||
suspend fun addToPlaylist(playlist: Playlist, songs: List<Song>)
|
||||
suspend fun addToPlaylist(playlist: Playlist, songs: List<Song>): Boolean
|
||||
|
||||
/**
|
||||
* Update the [Song]s of a [Playlist].
|
||||
*
|
||||
* @param playlist The [Playlist] to update.
|
||||
* @param songs The new [Song]s to be contained in the [Playlist].
|
||||
* @return True if the [Playlist] was successfully updated, false otherwise.
|
||||
*/
|
||||
suspend fun rewritePlaylist(playlist: Playlist, songs: List<Song>)
|
||||
suspend fun rewritePlaylist(playlist: Playlist, songs: List<Song>): Boolean
|
||||
}
|
||||
|
||||
class UserLibraryFactoryImpl
|
||||
@Inject
|
||||
constructor(private val playlistDao: PlaylistDao, private val musicSettings: MusicSettings) :
|
||||
UserLibrary.Factory {
|
||||
override suspend fun read(deviceLibraryChannel: Channel<DeviceLibrary>): MutableUserLibrary {
|
||||
// While were waiting for the library, read our playlists out.
|
||||
val rawPlaylists =
|
||||
try {
|
||||
playlistDao.readRawPlaylists()
|
||||
} catch (e: Exception) {
|
||||
logE("Unable to read playlists: $e")
|
||||
return UserLibraryImpl(playlistDao, mutableMapOf(), musicSettings)
|
||||
}
|
||||
override suspend fun query() =
|
||||
try {
|
||||
playlistDao.readRawPlaylists()
|
||||
} catch (e: Exception) {
|
||||
logE("Unable to read playlists: $e")
|
||||
listOf()
|
||||
}
|
||||
|
||||
override suspend fun create(
|
||||
rawPlaylists: List<RawPlaylist>,
|
||||
deviceLibrary: DeviceLibrary
|
||||
): MutableUserLibrary {
|
||||
logD("Successfully read ${rawPlaylists.size} playlists")
|
||||
val deviceLibrary = deviceLibraryChannel.receive()
|
||||
// Convert the database playlist information to actual usable playlists.
|
||||
val playlistMap = mutableMapOf<Music.UID, PlaylistImpl>()
|
||||
for (rawPlaylist in rawPlaylists) {
|
||||
|
@ -153,89 +173,106 @@ private class UserLibraryImpl(
|
|||
override fun equals(other: Any?) = other is UserLibraryImpl && other.playlistMap == playlistMap
|
||||
override fun toString() = "UserLibrary(playlists=${playlists.size})"
|
||||
|
||||
override val playlists: List<Playlist>
|
||||
get() = playlistMap.values.toList()
|
||||
override val playlists: Collection<Playlist>
|
||||
get() = playlistMap.values.toSet()
|
||||
|
||||
override fun findPlaylist(uid: Music.UID) = playlistMap[uid]
|
||||
|
||||
override fun findPlaylist(name: String) = playlistMap.values.find { it.name.raw == name }
|
||||
|
||||
override suspend fun createPlaylist(name: String, songs: List<Song>) {
|
||||
// TODO: Use synchronized with value access too
|
||||
override suspend fun createPlaylist(name: String, songs: List<Song>): Playlist? {
|
||||
val playlistImpl = PlaylistImpl.from(name, songs, musicSettings)
|
||||
synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl }
|
||||
val rawPlaylist =
|
||||
RawPlaylist(
|
||||
PlaylistInfo(playlistImpl.uid, playlistImpl.name.raw),
|
||||
playlistImpl.songs.map { PlaylistSong(it.uid) })
|
||||
try {
|
||||
|
||||
return try {
|
||||
playlistDao.insertPlaylist(rawPlaylist)
|
||||
logD("Successfully created playlist $name with ${songs.size} songs")
|
||||
playlistImpl
|
||||
} catch (e: Exception) {
|
||||
logE("Unable to create playlist $name with ${songs.size} songs")
|
||||
logE(e.stackTraceToString())
|
||||
synchronized(this) { playlistMap.remove(playlistImpl.uid) }
|
||||
return
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun renamePlaylist(playlist: Playlist, name: String) {
|
||||
override suspend fun renamePlaylist(playlist: Playlist, name: String): Boolean {
|
||||
val playlistImpl =
|
||||
requireNotNull(playlistMap[playlist.uid]) { "Cannot rename invalid playlist" }
|
||||
synchronized(this) { playlistMap[playlist.uid] = playlistImpl.edit(name, musicSettings) }
|
||||
try {
|
||||
synchronized(this) {
|
||||
requireNotNull(playlistMap[playlist.uid]) { "Cannot rename invalid playlist" }
|
||||
.also { playlistMap[it.uid] = it.edit(name, musicSettings) }
|
||||
}
|
||||
|
||||
return try {
|
||||
playlistDao.replacePlaylistInfo(PlaylistInfo(playlist.uid, name))
|
||||
logD("Successfully renamed $playlist to $name")
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
logE("Unable to rename $playlist to $name: $e")
|
||||
logE(e.stackTraceToString())
|
||||
synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl }
|
||||
return
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun deletePlaylist(playlist: Playlist) {
|
||||
override suspend fun deletePlaylist(playlist: Playlist): Boolean {
|
||||
val playlistImpl =
|
||||
requireNotNull(playlistMap[playlist.uid]) { "Cannot remove invalid playlist" }
|
||||
synchronized(this) { playlistMap.remove(playlistImpl.uid) }
|
||||
try {
|
||||
synchronized(this) {
|
||||
requireNotNull(playlistMap[playlist.uid]) { "Cannot remove invalid playlist" }
|
||||
.also { playlistMap.remove(it.uid) }
|
||||
}
|
||||
|
||||
return try {
|
||||
playlistDao.deletePlaylist(playlist.uid)
|
||||
logD("Successfully deleted $playlist")
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
logE("Unable to delete $playlist: $e")
|
||||
logE(e.stackTraceToString())
|
||||
synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl }
|
||||
return
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun addToPlaylist(playlist: Playlist, songs: List<Song>) {
|
||||
override suspend fun addToPlaylist(playlist: Playlist, songs: List<Song>): Boolean {
|
||||
val playlistImpl =
|
||||
requireNotNull(playlistMap[playlist.uid]) { "Cannot add to invalid playlist" }
|
||||
synchronized(this) { playlistMap[playlist.uid] = playlistImpl.edit { addAll(songs) } }
|
||||
try {
|
||||
synchronized(this) {
|
||||
requireNotNull(playlistMap[playlist.uid]) { "Cannot add to invalid playlist" }
|
||||
.also { playlistMap[it.uid] = it.edit { addAll(songs) } }
|
||||
}
|
||||
|
||||
return try {
|
||||
playlistDao.insertPlaylistSongs(playlist.uid, songs.map { PlaylistSong(it.uid) })
|
||||
logD("Successfully added ${songs.size} songs to $playlist")
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
logE("Unable to add ${songs.size} songs to $playlist: $e")
|
||||
logE(e.stackTraceToString())
|
||||
synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl }
|
||||
return
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun rewritePlaylist(playlist: Playlist, songs: List<Song>) {
|
||||
override suspend fun rewritePlaylist(playlist: Playlist, songs: List<Song>): Boolean {
|
||||
val playlistImpl =
|
||||
requireNotNull(playlistMap[playlist.uid]) { "Cannot rewrite invalid playlist" }
|
||||
synchronized(this) { playlistMap[playlist.uid] = playlistImpl.edit(songs) }
|
||||
try {
|
||||
synchronized(this) {
|
||||
requireNotNull(playlistMap[playlist.uid]) { "Cannot rewrite invalid playlist" }
|
||||
.also { playlistMap[it.uid] = it.edit(songs) }
|
||||
}
|
||||
|
||||
return try {
|
||||
playlistDao.replacePlaylistSongs(playlist.uid, songs.map { PlaylistSong(it.uid) })
|
||||
logD("Successfully rewrote $playlist with ${songs.size} songs")
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
logE("Unable to rewrite $playlist with ${songs.size} songs: $e")
|
||||
logE(e.stackTraceToString())
|
||||
synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl }
|
||||
return
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,7 +43,5 @@ class UserRoomModule {
|
|||
Room.databaseBuilder(
|
||||
context.applicationContext, UserMusicDatabase::class.java, "user_music.db")
|
||||
.fallbackToDestructiveMigration()
|
||||
.fallbackToDestructiveMigrationFrom(0)
|
||||
.fallbackToDestructiveMigrationOnDowngrade()
|
||||
.build()
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ import androidx.room.PrimaryKey
|
|||
import androidx.room.Query
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import androidx.room.migration.Migration
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||
|
||||
|
@ -37,7 +38,7 @@ import org.oxycblt.auxio.playback.state.RepeatMode
|
|||
*/
|
||||
@Database(
|
||||
entities = [PlaybackState::class, QueueHeapItem::class, QueueMappingItem::class],
|
||||
version = 27,
|
||||
version = 32,
|
||||
exportSchema = false)
|
||||
@TypeConverters(Music.UID.TypeConverters::class)
|
||||
abstract class PersistenceDatabase : RoomDatabase() {
|
||||
|
@ -54,6 +55,16 @@ abstract class PersistenceDatabase : RoomDatabase() {
|
|||
* @return A [QueueDao] providing control of the database's queue tables.
|
||||
*/
|
||||
abstract fun queueDao(): QueueDao
|
||||
|
||||
companion object {
|
||||
val MIGRATION_27_32 =
|
||||
Migration(27, 32) {
|
||||
// Switched from custom names to just letting room pick the names
|
||||
it.execSQL("ALTER TABLE playback_state RENAME TO PlaybackState")
|
||||
it.execSQL("ALTER TABLE queue_heap RENAME TO QueueHeapItem")
|
||||
it.execSQL("ALTER TABLE queue_mapping RENAME TO QueueMappingItem")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -68,11 +79,10 @@ interface PlaybackStateDao {
|
|||
*
|
||||
* @return The previously persisted [PlaybackState], or null if one was not present.
|
||||
*/
|
||||
@Query("SELECT * FROM ${PlaybackState.TABLE_NAME} WHERE id = 0")
|
||||
suspend fun getState(): PlaybackState?
|
||||
@Query("SELECT * FROM PlaybackState WHERE id = 0") suspend fun getState(): PlaybackState?
|
||||
|
||||
/** Delete any previously persisted [PlaybackState]s. */
|
||||
@Query("DELETE FROM ${PlaybackState.TABLE_NAME}") suspend fun nukeState()
|
||||
@Query("DELETE FROM PlaybackState") suspend fun nukeState()
|
||||
|
||||
/**
|
||||
* Insert a new [PlaybackState] into the database.
|
||||
|
@ -94,21 +104,20 @@ interface QueueDao {
|
|||
*
|
||||
* @return A list of persisted [QueueHeapItem]s wrapping each heap item.
|
||||
*/
|
||||
@Query("SELECT * FROM ${QueueHeapItem.TABLE_NAME}") suspend fun getHeap(): List<QueueHeapItem>
|
||||
@Query("SELECT * FROM QueueHeapItem") suspend fun getHeap(): List<QueueHeapItem>
|
||||
|
||||
/**
|
||||
* Get the previously persisted queue mapping.
|
||||
*
|
||||
* @return A list of persisted [QueueMappingItem]s wrapping each heap item.
|
||||
*/
|
||||
@Query("SELECT * FROM ${QueueMappingItem.TABLE_NAME}")
|
||||
suspend fun getMapping(): List<QueueMappingItem>
|
||||
@Query("SELECT * FROM QueueMappingItem") suspend fun getMapping(): List<QueueMappingItem>
|
||||
|
||||
/** Delete any previously persisted queue heap entries. */
|
||||
@Query("DELETE FROM ${QueueHeapItem.TABLE_NAME}") suspend fun nukeHeap()
|
||||
@Query("DELETE FROM QueueHeapItem") suspend fun nukeHeap()
|
||||
|
||||
/** Delete any previously persisted queue mapping entries. */
|
||||
@Query("DELETE FROM ${QueueMappingItem.TABLE_NAME}") suspend fun nukeMapping()
|
||||
@Query("DELETE FROM QueueMappingItem") suspend fun nukeMapping()
|
||||
|
||||
/**
|
||||
* Insert new heap entries into the database.
|
||||
|
@ -128,7 +137,7 @@ interface QueueDao {
|
|||
|
||||
// TODO: Figure out how to get RepeatMode to map to an int instead of a string
|
||||
// TODO: Use intrinsic table names rather than custom names
|
||||
@Entity(tableName = PlaybackState.TABLE_NAME)
|
||||
@Entity
|
||||
data class PlaybackState(
|
||||
@PrimaryKey val id: Int,
|
||||
val index: Int,
|
||||
|
@ -136,26 +145,9 @@ data class PlaybackState(
|
|||
val repeatMode: RepeatMode,
|
||||
val songUid: Music.UID,
|
||||
val parentUid: Music.UID?
|
||||
) {
|
||||
companion object {
|
||||
const val TABLE_NAME = "playback_state"
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@Entity(tableName = QueueHeapItem.TABLE_NAME)
|
||||
data class QueueHeapItem(@PrimaryKey val id: Int, val uid: Music.UID) {
|
||||
companion object {
|
||||
const val TABLE_NAME = "queue_heap"
|
||||
}
|
||||
}
|
||||
@Entity data class QueueHeapItem(@PrimaryKey val id: Int, val uid: Music.UID)
|
||||
|
||||
@Entity(tableName = QueueMappingItem.TABLE_NAME)
|
||||
data class QueueMappingItem(
|
||||
@PrimaryKey val id: Int,
|
||||
val orderedIndex: Int,
|
||||
val shuffledIndex: Int
|
||||
) {
|
||||
companion object {
|
||||
const val TABLE_NAME = "queue_mapping"
|
||||
}
|
||||
}
|
||||
@Entity
|
||||
data class QueueMappingItem(@PrimaryKey val id: Int, val orderedIndex: Int, val shuffledIndex: Int)
|
||||
|
|
|
@ -45,8 +45,7 @@ class PersistenceRoomModule {
|
|||
PersistenceDatabase::class.java,
|
||||
"playback_persistence.db")
|
||||
.fallbackToDestructiveMigration()
|
||||
.fallbackToDestructiveMigrationFrom(1)
|
||||
.fallbackToDestructiveMigrationOnDowngrade()
|
||||
.addMigrations(PersistenceDatabase.MIGRATION_27_32)
|
||||
.build()
|
||||
|
||||
@Provides fun playbackStateDao(database: PersistenceDatabase) = database.playbackStateDao()
|
||||
|
|
|
@ -81,7 +81,7 @@ class PlayFromArtistDialog :
|
|||
|
||||
override fun onDestroyBinding(binding: DialogMusicChoicesBinding) {
|
||||
super.onDestroyBinding(binding)
|
||||
choiceAdapter
|
||||
binding.choiceRecycler.adapter = null
|
||||
}
|
||||
|
||||
override fun onClick(item: Artist, viewHolder: RecyclerView.ViewHolder) {
|
||||
|
|
|
@ -81,7 +81,7 @@ class PlayFromGenreDialog :
|
|||
|
||||
override fun onDestroyBinding(binding: DialogMusicChoicesBinding) {
|
||||
super.onDestroyBinding(binding)
|
||||
choiceAdapter
|
||||
binding.choiceRecycler.adapter = null
|
||||
}
|
||||
|
||||
override fun onClick(item: Genre, viewHolder: RecyclerView.ViewHolder) {
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -21,15 +21,16 @@ package org.oxycblt.auxio.playback.replaygain
|
|||
import androidx.media3.common.C
|
||||
import androidx.media3.common.Format
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.Tracks
|
||||
import androidx.media3.common.audio.AudioProcessor
|
||||
import androidx.media3.exoplayer.audio.BaseAudioProcessor
|
||||
import java.nio.ByteBuffer
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.pow
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.metadata.TextTags
|
||||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.PlaybackSettings
|
||||
import org.oxycblt.auxio.playback.queue.Queue
|
||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
|
@ -48,9 +49,7 @@ class ReplayGainAudioProcessor
|
|||
constructor(
|
||||
private val playbackManager: PlaybackStateManager,
|
||||
private val playbackSettings: PlaybackSettings
|
||||
) : BaseAudioProcessor(), Player.Listener, PlaybackSettings.Listener {
|
||||
private var lastFormat: Format? = null
|
||||
|
||||
) : BaseAudioProcessor(), PlaybackStateManager.Listener, PlaybackSettings.Listener {
|
||||
private var volume = 1f
|
||||
set(value) {
|
||||
field = value
|
||||
|
@ -58,51 +57,38 @@ constructor(
|
|||
flush()
|
||||
}
|
||||
|
||||
/**
|
||||
* Add this instance to the components required for it to function correctly.
|
||||
*
|
||||
* @param player The [Player] to attach to. Should already have this instance as an audio
|
||||
* processor.
|
||||
*/
|
||||
fun addToListeners(player: Player) {
|
||||
player.addListener(this)
|
||||
init {
|
||||
playbackManager.addListener(this)
|
||||
playbackSettings.registerListener(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove this instance from the components required for it to function correctly.
|
||||
*
|
||||
* @param player The [Player] to detach from. Should already have this instance as an audio
|
||||
* processor.
|
||||
*/
|
||||
fun releaseFromListeners(player: Player) {
|
||||
player.removeListener(this)
|
||||
/** Remove this instance from the components required for it to function correctly. */
|
||||
fun release() {
|
||||
playbackManager.removeListener(this)
|
||||
playbackSettings.unregisterListener(this)
|
||||
}
|
||||
|
||||
// --- OVERRIDES ---
|
||||
|
||||
override fun onTracksChanged(tracks: Tracks) {
|
||||
super.onTracksChanged(tracks)
|
||||
// Try to find the currently playing track so we can update the ReplayGain adjustment
|
||||
// based on it.
|
||||
for (group in tracks.groups) {
|
||||
if (group.isSelected) {
|
||||
for (i in 0 until group.length) {
|
||||
if (group.isTrackSelected(i)) {
|
||||
applyReplayGain(group.getTrackFormat(i))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun onIndexMoved(queue: Queue) {
|
||||
logD("Index moved, updating current song")
|
||||
applyReplayGain(queue.currentSong)
|
||||
}
|
||||
|
||||
override fun onQueueChanged(queue: Queue, change: Queue.Change) {
|
||||
// Other types of queue changes preserve the current song.
|
||||
if (change.type == Queue.Change.Type.SONG) {
|
||||
applyReplayGain(queue.currentSong)
|
||||
}
|
||||
// Nothing selected, apply nothing
|
||||
applyReplayGain(null)
|
||||
}
|
||||
override fun onNewPlayback(queue: Queue, parent: MusicParent?) {
|
||||
logD("New playback started, updating playback information")
|
||||
applyReplayGain(queue.currentSong)
|
||||
}
|
||||
|
||||
override fun onReplayGainSettingsChanged() {
|
||||
// ReplayGain config changed, we need to set it up again.
|
||||
applyReplayGain(lastFormat)
|
||||
applyReplayGain(playbackManager.queue.currentSong)
|
||||
}
|
||||
|
||||
// --- REPLAYGAIN PARSING ---
|
||||
|
@ -110,115 +96,63 @@ constructor(
|
|||
/**
|
||||
* Updates the volume adjustment based on the given [Format].
|
||||
*
|
||||
* @param format The [Format] of the currently playing track, or null if nothing is playing.
|
||||
* @param song The [Format] of the currently playing track, or null if nothing is playing.
|
||||
*/
|
||||
private fun applyReplayGain(format: Format?) {
|
||||
lastFormat = format
|
||||
val gain = parseReplayGain(format ?: return)
|
||||
private fun applyReplayGain(song: Song?) {
|
||||
if (song == null) {
|
||||
logD("Nothing playing, disabling adjustment")
|
||||
volume = 1f
|
||||
return
|
||||
}
|
||||
|
||||
logD("Applying ReplayGain adjustment for $song")
|
||||
|
||||
val gain = song.replayGainAdjustment
|
||||
val preAmp = playbackSettings.replayGainPreAmp
|
||||
|
||||
val adjust =
|
||||
if (gain != null) {
|
||||
logD("Found ReplayGain adjustment $gain")
|
||||
// ReplayGain is configurable, so determine what to do based off of the mode.
|
||||
val useAlbumGain =
|
||||
when (playbackSettings.replayGainMode) {
|
||||
// User wants track gain to be preferred. Default to album gain only if
|
||||
// there is no track gain.
|
||||
ReplayGainMode.TRACK -> {
|
||||
logD("Using track strategy")
|
||||
gain.track == 0f
|
||||
}
|
||||
// User wants album gain to be preferred. Default to track gain only if
|
||||
// here is no album gain.
|
||||
ReplayGainMode.ALBUM -> {
|
||||
logD("Using album strategy")
|
||||
gain.album != 0f
|
||||
}
|
||||
// User wants album gain to be used when in an album, track gain otherwise.
|
||||
ReplayGainMode.DYNAMIC -> {
|
||||
logD("Using dynamic strategy")
|
||||
playbackManager.parent is Album &&
|
||||
playbackManager.queue.currentSong?.album == playbackManager.parent
|
||||
}
|
||||
// ReplayGain is configurable, so determine what to do based off of the mode.
|
||||
val resolvedAdjustment =
|
||||
when (playbackSettings.replayGainMode) {
|
||||
// User wants track gain to be preferred. Default to album gain only if
|
||||
// there is no track gain.
|
||||
ReplayGainMode.TRACK -> {
|
||||
logD("Using track strategy")
|
||||
gain.track ?: gain.album
|
||||
}
|
||||
// User wants album gain to be preferred. Default to track gain only if
|
||||
// here is no album gain.
|
||||
ReplayGainMode.ALBUM -> {
|
||||
logD("Using album strategy")
|
||||
gain.album ?: gain.track
|
||||
}
|
||||
// User wants album gain to be used when in an album, track gain otherwise.
|
||||
ReplayGainMode.DYNAMIC -> {
|
||||
logD("Using dynamic strategy")
|
||||
gain.album?.takeIf {
|
||||
playbackManager.parent is Album &&
|
||||
playbackManager.queue.currentSong?.album == playbackManager.parent
|
||||
}
|
||||
?: gain.track
|
||||
}
|
||||
}
|
||||
|
||||
val resolvedGain =
|
||||
if (useAlbumGain) {
|
||||
logD("Using album gain")
|
||||
gain.album
|
||||
} else {
|
||||
logD("Using track gain")
|
||||
gain.track
|
||||
}
|
||||
|
||||
// Apply the adjustment specified when there is ReplayGain tags.
|
||||
resolvedGain + preAmp.with
|
||||
val amplifiedAdjustment =
|
||||
if (resolvedAdjustment != null) {
|
||||
// Successfully resolved an adjustment, apply the corresponding pre-amp
|
||||
logD("Applying with pre-amp")
|
||||
resolvedAdjustment + preAmp.with
|
||||
} else {
|
||||
// No ReplayGain tags existed, or no tags were parsable, or there was no metadata
|
||||
// in the first place. Return the gain to use when there is no ReplayGain value.
|
||||
logD("No ReplayGain tags present")
|
||||
// No adjustment found, use the corresponding user-defined pre-amp
|
||||
logD("Applying without pre-amp")
|
||||
preAmp.without
|
||||
}
|
||||
|
||||
logD("Applying ReplayGain adjustment ${adjust}db")
|
||||
logD("Applying ReplayGain adjustment ${amplifiedAdjustment}db")
|
||||
|
||||
// Final adjustment along the volume curve.
|
||||
volume = 10f.pow(adjust / 20f)
|
||||
volume = 10f.pow(amplifiedAdjustment / 20f)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse ReplayGain information from the given [Format].
|
||||
*
|
||||
* @param format The [Format] to parse.
|
||||
* @return A [Adjustment] adjustment, or null if there were no valid adjustments.
|
||||
*/
|
||||
private fun parseReplayGain(format: Format): Adjustment? {
|
||||
val textTags = TextTags(format.metadata ?: return null)
|
||||
var trackGain = 0f
|
||||
var albumGain = 0f
|
||||
|
||||
// Most ReplayGain tags are formatted as a simple decibel adjustment in a custom
|
||||
// replaygain_*_gain tag.
|
||||
textTags.id3v2["TXXX:$TAG_RG_TRACK_GAIN"]
|
||||
?.run { first().parseReplayGainAdjustment() }
|
||||
?.let { trackGain = it }
|
||||
textTags.id3v2["TXXX:$TAG_RG_ALBUM_GAIN"]
|
||||
?.run { first().parseReplayGainAdjustment() }
|
||||
?.let { albumGain = it }
|
||||
textTags.vorbis[TAG_RG_ALBUM_GAIN]
|
||||
?.run { first().parseReplayGainAdjustment() }
|
||||
?.let { trackGain = it }
|
||||
textTags.vorbis[TAG_RG_TRACK_GAIN]
|
||||
?.run { first().parseReplayGainAdjustment() }
|
||||
?.let { albumGain = it }
|
||||
|
||||
// Opus has it's own "r128_*_gain" ReplayGain specification, which requires dividing the
|
||||
// adjustment by 256 to get the gain. This is used alongside the base adjustment
|
||||
// intrinsic to the format to create the normalized adjustment. This is normally the only
|
||||
// tag used for opus files, but some software still writes replay gain tags anyway.
|
||||
textTags.vorbis[TAG_R128_TRACK_GAIN]
|
||||
?.run { first().parseReplayGainAdjustment() }
|
||||
?.let { trackGain = it / 256f }
|
||||
textTags.vorbis[TAG_R128_ALBUM_GAIN]
|
||||
?.run { first().parseReplayGainAdjustment() }
|
||||
?.let { albumGain = it / 256f }
|
||||
|
||||
return if (trackGain != 0f || albumGain != 0f) {
|
||||
Adjustment(trackGain, albumGain)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a ReplayGain adjustment into a float value.
|
||||
*
|
||||
* @return A parsed adjustment float, or null if the adjustment had invalid formatting.
|
||||
*/
|
||||
private fun String.parseReplayGainAdjustment() =
|
||||
replace(REPLAYGAIN_ADJUSTMENT_FILTER_REGEX, "").toFloatOrNull()
|
||||
|
||||
// --- AUDIO PROCESSOR IMPLEMENTATION ---
|
||||
|
||||
override fun onConfigure(
|
||||
|
@ -284,25 +218,4 @@ constructor(
|
|||
put(short.toByte())
|
||||
put(short.toInt().shr(8).toByte())
|
||||
}
|
||||
|
||||
/**
|
||||
* The resolved ReplayGain adjustment for a file.
|
||||
*
|
||||
* @param track The track adjustment (in dB), or 0 if it is not present.
|
||||
* @param album The album adjustment (in dB), or 0 if it is not present.
|
||||
*/
|
||||
private data class Adjustment(val track: Float, val album: Float)
|
||||
|
||||
private companion object {
|
||||
const val TAG_RG_TRACK_GAIN = "replaygain_track_gain"
|
||||
const val TAG_RG_ALBUM_GAIN = "replaygain_album_gain"
|
||||
const val TAG_R128_TRACK_GAIN = "r128_track_gain"
|
||||
const val TAG_R128_ALBUM_GAIN = "r128_album_gain"
|
||||
|
||||
/**
|
||||
* Matches non-float information from ReplayGain adjustments. Derived from vanilla music:
|
||||
* https://github.com/vanilla-music/vanilla
|
||||
*/
|
||||
val REPLAYGAIN_ADJUSTMENT_FILTER_REGEX by lazy { Regex("[^\\d.-]") }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -367,7 +367,7 @@ constructor(
|
|||
.setSubtitle(song.artists.resolveNames(context))
|
||||
// Since we usually have to load many songs into the queue, use the
|
||||
// MediaStore URI instead of loading a bitmap.
|
||||
.setIconUri(song.album.coverUri)
|
||||
.setIconUri(song.album.coverUri.mediaStore)
|
||||
.setMediaUri(song.uri)
|
||||
.build()
|
||||
// Store the item index so we can then use the analogous index in the
|
||||
|
|
|
@ -144,7 +144,6 @@ class PlaybackService :
|
|||
true)
|
||||
.build()
|
||||
.also { it.addListener(this) }
|
||||
replayGainProcessor.addToListeners(player)
|
||||
foregroundManager = ForegroundManager(this)
|
||||
// Initialize any listener-dependent components last as we wouldn't want a listener race
|
||||
// condition to cause us to load music before we were fully initialize.
|
||||
|
@ -196,7 +195,7 @@ class PlaybackService :
|
|||
widgetComponent.release()
|
||||
mediaSessionComponent.release()
|
||||
|
||||
replayGainProcessor.releaseFromListeners(player)
|
||||
replayGainProcessor.release()
|
||||
player.release()
|
||||
if (openAudioEffectSession) {
|
||||
// Make sure to close the audio session when we release the player.
|
||||
|
|
|
@ -58,11 +58,11 @@ interface SearchEngine {
|
|||
* @param playlists A list of [Playlist], null if empty.
|
||||
*/
|
||||
data class Items(
|
||||
val songs: List<Song>?,
|
||||
val albums: List<Album>?,
|
||||
val artists: List<Artist>?,
|
||||
val genres: List<Genre>?,
|
||||
val playlists: List<Playlist>?
|
||||
val songs: Collection<Song>?,
|
||||
val albums: Collection<Album>?,
|
||||
val artists: Collection<Artist>?,
|
||||
val genres: Collection<Genre>?,
|
||||
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
|
||||
* result quality.
|
||||
*/
|
||||
private inline fun <T : Music> List<T>.searchListImpl(
|
||||
private inline fun <T : Music> Collection<T>.searchListImpl(
|
||||
query: String,
|
||||
fallback: (String, T) -> Boolean = { _, _ -> false }
|
||||
) =
|
||||
|
|
|
@ -286,7 +286,7 @@ fun Context.share(parent: MusicParent) = share(parent.songs)
|
|||
*
|
||||
* @param songs The [Song]s to share.
|
||||
*/
|
||||
fun Context.share(songs: List<Song>) {
|
||||
fun Context.share(songs: Collection<Song>) {
|
||||
if (songs.isEmpty()) return
|
||||
logD("Showing sharesheet for ${songs.size} songs")
|
||||
val builder = ShareCompat.IntentBuilder(this)
|
||||
|
|
|
@ -50,6 +50,13 @@ fun Int.nonZeroOrNull() = if (this > 0) this else null
|
|||
*/
|
||||
fun Long.nonZeroOrNull() = if (this > 0) this else null
|
||||
|
||||
/**
|
||||
* Aliases a check to ensure that the given number is non-zero.
|
||||
*
|
||||
* @return The same number if it's non-zero, null otherwise.
|
||||
*/
|
||||
fun Float.nonZeroOrNull() = if (this > 0) this else null
|
||||
|
||||
/**
|
||||
* Aliases a check to ensure a given value is in a specified range.
|
||||
*
|
||||
|
|
|
@ -169,7 +169,7 @@
|
|||
<string name="cdc_mka">Matroska-Audio</string>
|
||||
<string name="cdc_aac">Advanced Audio Coding (AAC)</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_indexing">Lade deine Musikbibliothek… (%1$d/%2$d)</string>
|
||||
<string name="lbl_shuffle_shortcut_short">Mischen</string>
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
<string name="lbl_genre">Tyylilaji</string>
|
||||
<string name="lbl_genres">Tyylilajit</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_playlists">Soittolistat</string>
|
||||
<string name="lbl_search">Etsi</string>
|
||||
|
@ -164,7 +164,7 @@
|
|||
<string name="lbl_album_remix">Remix-albumi</string>
|
||||
<string name="lbl_ep_remix">Remix-EP</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="lbl_version">Versio</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_round_mode">Pyöristetty tila</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="set_root_title">Asetukset</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_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="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>
|
|
@ -41,7 +41,7 @@
|
|||
<string name="err_no_music">Pas de musique trouvée</string>
|
||||
<!-- Description Namespace | Accessibility Strings -->
|
||||
<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 -->
|
||||
<string name="lng_search_library">Recherche dans votre bibliothèque…</string>
|
||||
<!-- Color Label namespace | Accent names -->
|
||||
|
@ -50,26 +50,26 @@
|
|||
<string name="clr_purple">Violet</string>
|
||||
<string name="clr_indigo">Indigo</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_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_yellow">Jaune</string>
|
||||
<string name="clr_orange">Orange</string>
|
||||
<string name="clr_brown">Brun</string>
|
||||
<string name="clr_grey">Gris</string>
|
||||
<!-- 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">
|
||||
<item quantity="one">%s Titre</item>
|
||||
<item quantity="many">%s Titres</item>
|
||||
<item quantity="other">%s Titres</item>
|
||||
<item quantity="one">%d titre</item>
|
||||
<item quantity="many">%d titres</item>
|
||||
<item quantity="other">%d titres</item>
|
||||
</plurals>
|
||||
<plurals name="fmt_album_count">
|
||||
<item quantity="one">%s Album</item>
|
||||
<item quantity="many">%s Albums</item>
|
||||
<item quantity="other">%s Albums</item>
|
||||
<item quantity="one">%d album</item>
|
||||
<item quantity="many">%d albums</item>
|
||||
<item quantity="other">%d albums</item>
|
||||
</plurals>
|
||||
<string name="lbl_format">Format</string>
|
||||
<string name="lbl_state_saved">État sauvegardé</string>
|
||||
|
@ -92,7 +92,7 @@
|
|||
<string name="set_display">Affichage</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="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_indexing">Chargement de votre bibliothèque musicale…</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_props">Propriétés de la chanson</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_remix">Single remixé</string>
|
||||
<string name="lbl_compilations">Compilations</string>
|
||||
|
@ -114,7 +114,7 @@
|
|||
<string name="lbl_live_group">Live</string>
|
||||
<string name="lbl_indexing">Chargement de la musique</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_singles">Singles</string>
|
||||
<string name="lbl_single">Single</string>
|
||||
|
@ -125,7 +125,7 @@
|
|||
<string name="lbl_remix_group">Remix</string>
|
||||
<string name="lbl_date_added">Date d\'ajout</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_equalizer">Égaliseur</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="def_artist">Artiste inconnu</string>
|
||||
<string name="lbl_compilation_live">Compilation en direct</string>
|
||||
<string name="lbl_compilation_remix">Compilations de remix</string>
|
||||
<string name="lbl_mixes">Mixes</string>
|
||||
<string name="lbl_mix">Mix</string>
|
||||
<string name="lbl_compilation_remix">Compilation de remix</string>
|
||||
<string name="lbl_mixes">Mix DJ</string>
|
||||
<string name="lbl_mix">Mix DJ</string>
|
||||
<string name="err_bad_dir">Ce dossier n\'est pas pris en charge</string>
|
||||
<string name="lbl_reset">Réinitialiser</string>
|
||||
<string name="cdc_ogg">Ogg audio</string>
|
||||
|
@ -194,7 +194,7 @@
|
|||
<string name="set_separators_comma">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_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_playback_mode_album">Lire depuis l\'album</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_dirs_mode">Mode</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&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&nbsp;: %d</string>
|
||||
<string name="fmt_lib_total_duration">Durée totale&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&nbsp;\? Cette opération ne peut pas être annulée.</string>
|
||||
<string name="set_pre_amp_warning">Avertissement&nbsp;: Le fait de régler le préamplificateur sur une valeur positive élevée peut entraîner l\'apparition des distortions sur certaines pistes audio.</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&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&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>
|
|
@ -59,4 +59,132 @@
|
|||
<string name="lbl_indexer">गाने लोड हो रहे है</string>
|
||||
<string name="lbl_indexing">गाने लोड हो रहे है</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>
|
|
@ -149,10 +149,10 @@
|
|||
<string name="fmt_bitrate">%d kbps</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_lib_song_count">Učitano pjesama: %d</string>
|
||||
<string name="fmt_lib_album_count">Učitano albuma: %d</string>
|
||||
<string name="fmt_lib_artist_count">Učitanih izvođača: %d</string>
|
||||
<string name="fmt_lib_genre_count">Učitano žanrova: %d</string>
|
||||
<string name="fmt_lib_song_count">Broj učitanih pjesama: %d</string>
|
||||
<string name="fmt_lib_album_count">Broj učitanih albuma: %d</string>
|
||||
<string name="fmt_lib_artist_count">Broj učitanih izvođača: %d</string>
|
||||
<string name="fmt_lib_genre_count">Broj učitanih žanrova: %d</string>
|
||||
<string name="fmt_lib_total_duration">Ukupno trajanje: %s</string>
|
||||
<plurals name="fmt_song_count">
|
||||
<item quantity="one">%d pjesma</item>
|
||||
|
@ -215,13 +215,13 @@
|
|||
<string name="set_separators_and">Ampersand (&)</string>
|
||||
<string name="lbl_compilation_live">Kompilacija uživo</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="desc_exit">Prekini reprodukciju</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_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_bar_action">Prilagođena radnja trake reprodukcije</string>
|
||||
<string name="lbl_equalizer">Ekvilajzer</string>
|
||||
|
@ -286,4 +286,8 @@
|
|||
<string name="lng_playlist_added">Dodano u popis pjesama</string>
|
||||
<string name="lbl_edit">Uredi</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>
|
|
@ -8,7 +8,7 @@
|
|||
<string name="lbl_grant">Permetti</string>
|
||||
<string name="lbl_genres">Generi</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_all_songs">Tutte le canzoni</string>
|
||||
<string name="lbl_search">Cerca</string>
|
||||
|
@ -17,21 +17,21 @@
|
|||
<string name="lbl_sort">Ordine</string>
|
||||
<string name="lbl_name">Nome</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_sort_asc">Ascendente</string>
|
||||
<string name="lbl_playback">Ora in riproduzione</string>
|
||||
<string name="lbl_play">Riproduci</string>
|
||||
<string name="lbl_shuffle">Mescola</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="lbl_queue">Coda</string>
|
||||
<string name="lbl_play_next">Riproduci successivo</string>
|
||||
<string name="lbl_queue_add">Accoda</string>
|
||||
<string name="lng_queue_added">Accodato</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_add">Aggiungi</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_music_dir_delete">Rimuovi cartella</string>
|
||||
<string name="desc_auxio_icon">Icona Auxio</string>
|
||||
<string name="desc_no_cover">Copertina disco</string>
|
||||
<string name="desc_album_cover">Copertina disco per %s</string>
|
||||
<string name="desc_no_cover">Copertina album</string>
|
||||
<string name="desc_album_cover">Copertina album per %s</string>
|
||||
<string name="desc_artist_image">Immagine artista per %s</string>
|
||||
<string name="desc_genre_image">Immagine genere per %s</string>
|
||||
<!-- Default Namespace | Placeholder values -->
|
||||
|
@ -129,7 +129,7 @@
|
|||
<string name="clr_grey">Grigio</string>
|
||||
<!-- Format Namespace | Value formatting/plurals -->
|
||||
<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_genre_count">Generi trovati: %d</string>
|
||||
<string name="fmt_lib_total_duration">Durata totale: %s</string>
|
||||
|
@ -139,9 +139,9 @@
|
|||
<item quantity="other">%d canzoni</item>
|
||||
</plurals>
|
||||
<plurals name="fmt_album_count">
|
||||
<item quantity="one">%d disco</item>
|
||||
<item quantity="many">%d dischi</item>
|
||||
<item quantity="other">%d dischi</item>
|
||||
<item quantity="one">%d album</item>
|
||||
<item quantity="many">%d album</item>
|
||||
<item quantity="other">%d album</item>
|
||||
</plurals>
|
||||
<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>
|
||||
|
@ -169,7 +169,7 @@
|
|||
<string name="cdc_flac">Free Lossless Audio Codec (FLAC)</string>
|
||||
<string name="cdc_aac">Advanced Audio Coding (AAC)</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="lbl_indexer">Caricamento musica</string>
|
||||
<string name="lng_indexing">Caricamento libreria musicale…</string>
|
||||
|
@ -240,8 +240,8 @@
|
|||
<string name="set_separators_and">E commerciale (&)</string>
|
||||
<string name="lbl_compilation_live">Raccolte live</string>
|
||||
<string name="lbl_compilation_remix">Raccolta di remix</string>
|
||||
<string name="lbl_mixes">Mixes</string>
|
||||
<string name="lbl_mix">Mix</string>
|
||||
<string name="lbl_mixes">Mix DJ</string>
|
||||
<string name="lbl_mix">Mix DJ</string>
|
||||
<string name="set_cover_mode_quality">Alta qualità</string>
|
||||
<string name="set_separators_comma">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="lbl_shuffle_selected">Mescola 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="lbl_wiki">Wiki</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="lng_playlist_deleted">Playlist eliminata</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>
|
|
@ -9,7 +9,7 @@
|
|||
<string name="lbl_artists">Artiesten</string>
|
||||
<string name="lbl_albums">Albums</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_filter">Filter</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_album">Speel af van album </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_play_next">Afspelen als volgende</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_add">Toevoegen</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_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="lng_author">Ontwikkeld door OxygenCobalt</string>
|
||||
<string name="lng_author">Ontwikkeld door Alexander Capehart</string>
|
||||
<!-- Settings namespace | Settings-related labels -->
|
||||
<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_auto">Automatisch</string>
|
||||
<string name="set_theme_day">Licht</string>
|
||||
|
@ -60,36 +60,36 @@
|
|||
<string name="err_no_music">Geen muziek aangetroffen</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_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>
|
||||
<!-- Hint Namespace | EditText Hints -->
|
||||
<string name="lng_search_library">Zoek in uw bibliotheek…</string>
|
||||
<!-- Description Namespace | Accessibility Strings -->
|
||||
<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_prev">Naar het laatste nummer gaan</string>
|
||||
<string name="desc_change_repeat">Herhaalfunctie wijzigen</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_album_cover">Artist Image voor %s</string>
|
||||
<string name="desc_artist_image">Artist Image voor %s</string>
|
||||
<string name="desc_genre_image">Genre Image voor %s</string>
|
||||
<string name="desc_album_cover">Albumhoes voor %s</string>
|
||||
<string name="desc_artist_image">Artiesten-afbeelding voor %s</string>
|
||||
<string name="desc_genre_image">Genre-afbeelding voor %s</string>
|
||||
<!-- Placeholder Namespace | Placeholder values -->
|
||||
<string name="def_genre">Onbekend Genre</string>
|
||||
<string name="def_date">Geen datum </string>
|
||||
<string name="def_genre">Onbekend genre</string>
|
||||
<string name="def_date">Geen datum</string>
|
||||
<!-- Color Label namespace | Accent names -->
|
||||
<string name="clr_red">Rood</string>
|
||||
<string name="clr_pink">Roze</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_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_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_lime">Geelgroen</string>
|
||||
<string name="clr_yellow">Geel</string>
|
||||
|
@ -99,14 +99,14 @@
|
|||
<!-- Format Namespace | Value formatting/plurals -->
|
||||
<string name="fmt_lib_song_count">Nummers geladen: %d</string>
|
||||
<plurals name="fmt_song_count">
|
||||
<item quantity="one">%d Nummer</item>
|
||||
<item quantity="other">%d Nummers</item>
|
||||
<item quantity="one">%d lied</item>
|
||||
<item quantity="other">%d liedjes</item>
|
||||
</plurals>
|
||||
<plurals name="fmt_album_count">
|
||||
<item quantity="one">%d Album</item>
|
||||
<item quantity="other">%d Albums</item>
|
||||
<item quantity="one">%d album</item>
|
||||
<item quantity="other">%d albums</item>
|
||||
</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_desc">Gebruik een puur-zwart donker thema</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_name">Naam</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="lbl_date">Jaar</string>
|
||||
<string name="lbl_relative_path">Ouderpad</string>
|
||||
|
@ -133,25 +133,25 @@
|
|||
<string name="set_dirs_mode">Modus</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="lbl_shuffle_shortcut_long">Shuffle Alles</string>
|
||||
<string name="lbl_ok">@android:string/ok</string>
|
||||
<string name="lbl_shuffle_shortcut_long">Alles schudden</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="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="set_dirs_mode_exclude">Uitgezonderd</string>
|
||||
<string name="desc_shuffle_all">Alle liedjes shuffelen</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_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_pre_amp_with">Aanpassing met tags</string>
|
||||
<string name="set_pre_amp_without">Aanpassing zonder tags</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_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_library_counts">Bibliotheekstatistieken</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_restore_state">Afspeelstatus herstellen</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_song_handle">Verplaats dit wachtrij liedje</string>
|
||||
<string name="desc_tab_handle">Verplaats deze tab</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="clr_dynamic">Dynamisch</string>
|
||||
<string name="cdc_mp3">MPEG-1 Audio</string>
|
||||
<string name="cdc_mp4">MPEG-4 Audio</string>
|
||||
<string name="cdc_ogg">Ogg Audio</string>
|
||||
<string name="cdc_mka">Matroska Audio</string>
|
||||
<string name="cdc_mp3">MPEG-1 audio</string>
|
||||
<string name="cdc_mp4">MPEG-4-audio</string>
|
||||
<string name="cdc_ogg">Ogg audio</string>
|
||||
<string name="cdc_mka">Matroska-audio</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_genre_count">Genres geladen: %d</string>
|
||||
|
@ -189,4 +189,111 @@
|
|||
<string name="lbl_shuffle_shortcut_short">Shuffle</string>
|
||||
<string name="cdc_aac">Geavanceerde audio codering (GAC)</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 (&)</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>
|
|
@ -27,8 +27,8 @@
|
|||
<string name="lbl_soundtrack">ਸਾਊਂਡਟ੍ਰੈਕ</string>
|
||||
<string name="lbl_soundtracks">ਸਾਊਂਡਟ੍ਰੈਕਸ</string>
|
||||
<string name="lbl_mixtapes">ਮਿਕਸਟੇਪਸ</string>
|
||||
<string name="lbl_mixes">ਮਿਕਸ</string>
|
||||
<string name="lbl_mix">ਮਿਕਸ</string>
|
||||
<string name="lbl_mixes">ਡੀਜੇ ਮਿਕਸ</string>
|
||||
<string name="lbl_mix">ਡੀਜੇ ਮਿਕਸ</string>
|
||||
<string name="lbl_live_group">ਲਾਈਵ</string>
|
||||
<string name="lbl_remix_group">ਰੀਮਿਕਸ</string>
|
||||
<string name="lbl_artist">ਕਲਾਕਾਰ</string>
|
||||
|
@ -180,7 +180,7 @@
|
|||
<string name="set_separators_comma">ਕੌਮਾ (,)</string>
|
||||
<string name="set_separators_semicolon">ਸੈਮੀਕੋਲਨ (;)</string>
|
||||
<string name="set_separators_slash">ਸਲੈਸ਼ (/)</string>
|
||||
<string name="set_separators_and">ਐਂਪਰਸੈਂਡ (&)</string>
|
||||
<string name="set_separators_and">Ampersand (&)</string>
|
||||
<string name="set_hide_collaborators">ਸਹਿਯੋਗੀਆਂ ਨੂੰ ਲੁਕਾਓ</string>
|
||||
<string name="set_audio_desc">ਆਵਾਜ਼ ਅਤੇ ਪਲੇਬੈਕ ਵਿਵਹਾਰ ਦੀ ਸੰਰਚਨਾ ਕਰੋ</string>
|
||||
<string name="set_playback">ਪਲੇਅਬੈਕ</string>
|
||||
|
@ -202,4 +202,89 @@
|
|||
<string name="desc_shuffle">ਸ਼ਫਲ ਚਾਲੂ ਜਾਂ ਬੰਦ ਕਰੋ</string>
|
||||
<string name="desc_shuffle_all">ਸਾਰੇ ਗੀਤਾਂ ਨੂੰ ਸ਼ਫਲ ਕਰੋ</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>
|
|
@ -191,7 +191,7 @@
|
|||
<string name="lbl_album_live">Концертный альбом</string>
|
||||
<string name="lbl_live_group">Концертный</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_mode_include">Включить</string>
|
||||
<string name="lbl_album_remix">Альбом ремиксов</string>
|
||||
|
|
|
@ -275,4 +275,9 @@
|
|||
<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="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>
|
|
@ -27,7 +27,7 @@
|
|||
<string name="lbl_licenses">Ліцензії</string>
|
||||
<!-- Settings namespace | Settings-related labels -->
|
||||
<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_day">Світла</string>
|
||||
<string name="set_theme_night">Темна</string>
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
<?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>
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<data-extraction-rules>
|
||||
<cloud-backup />
|
||||
<device-transfer />
|
||||
<!-- Cache databases are useless between devices, drop them -->
|
||||
<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>
|
3
fastlane/metadata/android/en-US/changelogs/32.txt
Normal file
3
fastlane/metadata/android/en-US/changelogs/32.txt
Normal 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.
|
1
fastlane/metadata/android/fi/short_description.txt
Normal file
1
fastlane/metadata/android/fi/short_description.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Yksinkertainen ja rationaallinen musiikkisoitin
|
|
@ -10,7 +10,8 @@ Auxio एक तेज़, विश्वसनीय UI/UX वाला एक
|
|||
- डिस्क संख्या, एकाधिक कलाकार, रिलीज़ प्रकार, सटीक के लिए समर्थन / मूल दिनांक, सॉर्ट टैग, और बहुत कुछ
|
||||
- उन्नत कलाकार प्रणाली जो कलाकारों और एल्बम कलाकारों को एकजुट करती है
|
||||
- एसडी कार्ड-जागरूक फ़ोल्डर प्रबंधन
|
||||
- विश्वसनीय प्लेबैक स्थिति दृढ़ता
|
||||
- विश्वसनीय प्लेलिस्टिंग कार्यक्षमता
|
||||
- प्लेबैक अवस्था दृढ़ता
|
||||
- पूर्ण रीप्लेगैन समर्थन (MP3, FLAC, OGG, OPUS और MP4 फ़ाइलों पर)
|
||||
- बाहरी तुल्यकारक समर्थन (उदा। वेवलेट)
|
||||
- एज-टू-एज
|
||||
|
|
|
@ -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>
|
||||
|
||||
- Reprodukcija bazirana na ExoPlayeru
|
||||
- Brzo korisničko sučelje u skladu s najnovijim Materijal dizajnom
|
||||
- Korisničko iskustvo koje priorizira jednostavnost korištenja
|
||||
- Prilagodljive ponašanje aplikacije
|
||||
- Podrška za brojeve diskova, izvođače, vrste izdanja,
|
||||
precizne/izvorne datume, oznake razvrstavanje i još više
|
||||
- Reprodukcija temeljena na ExoPlayeru
|
||||
- Snappy UI izvedeno iz najnovijih smjernica za materijalni dizajn
|
||||
- Iskustveni korisnički doživljaj koji daje prednost jednostavnosti upotrebe u odnosu na rubne slučajeve
|
||||
- Prilagodljivo ponašanje
|
||||
- Podrška za brojeve diskova, više izvođača, vrste izdanja,
|
||||
precizni/izvorni datumi, sortiranje oznaka i više
|
||||
- Napredni sustav izvođača koji ujedinjuje izvođače i izvođače albuma
|
||||
- Upravljanje mapama SD kartica
|
||||
- Pouzdana postojanost stanja reprodukcije
|
||||
- Potpuna ReplayGain podrška (Za MP3, MP4, FLAC, OGG, i OPUS formate)
|
||||
- Podrška za eksterne ekvilajzere (npr. Wavelet)
|
||||
- Prikaz od ruba do ruba
|
||||
- Podrška za ugrađene omote
|
||||
- Pretražinje
|
||||
- Mogućnost pokretanja glazbe čim spojite slušalice
|
||||
- Stilizirani widgeti koji automatski prilagođavaju svoju veličinu
|
||||
- Potpuno privatan bez potrebe za internetskom vezom
|
||||
- Bez zaobljenih omota albuma (Osim ako ih želite. Onda ih možete imati.)
|
||||
- Upravljanje mapama koje podržava SD karticu
|
||||
- Pouzdana funkcija popisa pjesama
|
||||
- Postojanost stanja reprodukcije
|
||||
- Puna podrška za ReplayGain (na MP3, FLAC, OGG, OPUS i MP4 datotekama)
|
||||
- Podrška za vanjski ekvilizator (npr. Wavelet)
|
||||
- Od ruba do ruba
|
||||
- Podrška za ugrađene naslovnice
|
||||
- Funkcionalnost pretraživanja
|
||||
- Automatska reprodukcija slušalica
|
||||
- Elegantni widgeti koji se automatski prilagođavaju njihovoj veličini
|
||||
- Potpuno privatno i izvan mreže
|
||||
- Nema zaobljenih naslovnica albuma (Osim ako ih ne želite. Onda možete.)
|
||||
|
|
|
@ -6,12 +6,14 @@ Auxio는 다른 음악 플레이어에 존재하는 많은 쓸모없는 기능
|
|||
- 최신주목할 만한 디자인 가이드라인에서 파생된 Snappy UI
|
||||
- 엣지 케이스보다 사용 편의성을 우선시하는 의견이 많은 UX
|
||||
- 사용자 정의 가능한 동작
|
||||
- 올바른 메타데이터의 우선 순위를 지정하는 고급 미디어 인덱서
|
||||
- 정확한/원래 날짜, 정렬 태그 및 릴리스 유형 지원(실험적)
|
||||
- 디스크 번호, 여러 아티스트, 릴리스 유형 지원,
|
||||
정확한/원본 날짜, 정렬 태그 등 지원
|
||||
- 아티스트와 앨범 아티스트를 통합하는 고급 아티스트 시스템
|
||||
- SD 카드 인식 폴더 관리
|
||||
- 안정적인 재생 상태 지속성
|
||||
- 완전한 ReplayGain 지원 (MP3, MP4, FLAC, OGG, OPUS)
|
||||
- 외부 이퀄라이저 기능 (Wavelet과 같은 앱)
|
||||
- 안정적인 재생 목록 기능
|
||||
- 재생 상태 지속성
|
||||
- 전체 ReplayGain 지원 (MP3, FLAC, OGG, OPUS, MP4)
|
||||
- 외부 이퀄라이저 지원 (예: Wavelet)
|
||||
- Edge-to-edge
|
||||
- 임베디드 커버 지원
|
||||
- 검색 기능
|
||||
|
|
|
@ -8,7 +8,10 @@ Auxio ਇੱਕ ਤੇਜ਼, ਭਰੋਸੇਮੰਦ UI/UX ਵਾਲਾ ਇੱ
|
|||
- ਅਨੁਕੂਲਿਤ ਵਿਵਹਾਰ
|
||||
- ਡਿਸਕ ਨੰਬਰਾਂ, ਮਲਟੀਪਲ ਕਲਾਕਾਰਾਂ, ਰੀਲੀਜ਼ ਕਿਸਮਾਂ, ਸਟੀਕ ਲਈ ਸਮਰਥਨ /ਮੂਲ ਤਾਰੀਖਾਂ, ਕ੍ਰਮਬੱਧ ਟੈਗਸ, ਅਤੇ ਹੋਰ
|
||||
- ਉੱਨਤ ਕਲਾਕਾਰ ਪ੍ਰਣਾਲੀ ਜੋ ਕਲਾਕਾਰਾਂ ਅਤੇ ਐਲਬਮ ਕਲਾਕਾਰਾਂ ਨੂੰ ਇਕਜੁੱਟ ਕਰਦੀ ਹੈ
|
||||
- SD ਕਾਰਡ-ਜਾਣੂ ਫੋਲਡਰ ਪ੍ਰਬੰਧਨ - ਭਰੋਸੇਯੋਗ ਪਲੇਬੈਕ ਸਥਿਤੀ ਸਥਿਰਤਾ - ਪੂਰਾ ਰੀਪਲੇਗੇਨ ਸਮਰਥਨ (MP3, FLAC, OGG, OPUS, ਅਤੇ MP4 ਫਾਈਲਾਂ 'ਤੇ)
|
||||
- SD ਕਾਰਡ-ਜਾਣੂ ਫੋਲਡਰ ਪ੍ਰਬੰਧਨ
|
||||
- ਭਰੋਸੇਯੋਗ ਪਲੇਅਲਿਸਟਿੰਗ ਕਾਰਜਕੁਸ਼ਲਤਾ
|
||||
- ਭਰੋਸੇਯੋਗ ਪਲੇਅਬੈਕ ਸਥਿਤੀ ਸਥਿਰਤਾ
|
||||
- ਪੂਰਾ ਰੀਪਲੇਗੇਨ ਸਮਰਥਨ (MP3, FLAC, OGG, OPUS, ਅਤੇ MP4 ਫਾਈਲਾਂ 'ਤੇ)
|
||||
- ਬਾਹਰੀ ਈਕੋਲਾਈਜ਼ਰ ਦਾ ਸਮਰਥਨ (ਉਦਾਹਰਨ. ਵੇਵਲੇਟ)
|
||||
- ਕਿਨਾਰੇ-ਤੋਂ-ਕਿਨਾਰੇ
|
||||
- ਏਮਬੈਡਡ ਕਵਰ ਸਪੋਰਟ
|
||||
|
|
Loading…
Reference in a new issue