music: revamp paths

Revamp paths with an entirely new abstraction that should improve
testability and integration with M3U playlists.
This commit is contained in:
Alexander Capehart 2023-12-19 22:14:59 -07:00
parent 08ca71b7b0
commit 364675b252
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
50 changed files with 422 additions and 277 deletions

View file

@ -102,10 +102,7 @@ class SongDetailDialog : ViewBindingMaterialDialogFragment<DialogSongDetailBindi
} }
add(SongProperty(R.string.lbl_disc, zipped)) add(SongProperty(R.string.lbl_disc, zipped))
} }
add(SongProperty(R.string.lbl_file_name, song.path.name)) add(SongProperty(R.string.lbl_path, song.path.resolve(context)))
add(
SongProperty(
R.string.lbl_relative_path, song.path.parent.resolveName(context)))
info.resolvedMimeType.resolveName(context)?.let { info.resolvedMimeType.resolveName(context)?.let {
add(SongProperty(R.string.lbl_format, it)) add(SongProperty(R.string.lbl_format, it))
} }

View file

@ -24,8 +24,8 @@ import androidx.core.content.edit
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject import javax.inject.Inject
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.fs.Directory import org.oxycblt.auxio.music.dirs.DocumentTreePathFactory
import org.oxycblt.auxio.music.fs.MusicDirectories import org.oxycblt.auxio.music.dirs.MusicDirectories
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.getSystemServiceCompat import org.oxycblt.auxio.util.getSystemServiceCompat
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -55,8 +55,12 @@ interface MusicSettings : Settings<MusicSettings.Listener> {
} }
} }
class MusicSettingsImpl @Inject constructor(@ApplicationContext context: Context) : class MusicSettingsImpl
Settings.Impl<MusicSettings.Listener>(context), MusicSettings { @Inject
constructor(
@ApplicationContext context: Context,
val documentTreePathFactory: DocumentTreePathFactory
) : Settings.Impl<MusicSettings.Listener>(context), MusicSettings {
private val storageManager = context.getSystemServiceCompat(StorageManager::class) private val storageManager = context.getSystemServiceCompat(StorageManager::class)
override var musicDirs: MusicDirectories override var musicDirs: MusicDirectories
@ -64,7 +68,7 @@ class MusicSettingsImpl @Inject constructor(@ApplicationContext context: Context
val dirs = val dirs =
(sharedPreferences.getStringSet(getString(R.string.set_key_music_dirs), null) (sharedPreferences.getStringSet(getString(R.string.set_key_music_dirs), null)
?: emptySet()) ?: emptySet())
.mapNotNull { Directory.fromDocumentTreeUri(storageManager, it) } .mapNotNull(documentTreePathFactory::deserializeDocumentTreePath)
return MusicDirectories( return MusicDirectories(
dirs, dirs,
sharedPreferences.getBoolean(getString(R.string.set_key_music_dirs_include), false)) sharedPreferences.getBoolean(getString(R.string.set_key_music_dirs_include), false))
@ -73,7 +77,7 @@ class MusicSettingsImpl @Inject constructor(@ApplicationContext context: Context
sharedPreferences.edit { sharedPreferences.edit {
putStringSet( putStringSet(
getString(R.string.set_key_music_dirs), getString(R.string.set_key_music_dirs),
value.dirs.map(Directory::toDocumentTreeUri).toSet()) value.dirs.map(documentTreePathFactory::serializeDocumentTreePath).toSet())
putBoolean(getString(R.string.set_key_music_dirs_include), value.shouldInclude) putBoolean(getString(R.string.set_key_music_dirs_include), value.shouldInclude)
apply() apply()
} }

View file

@ -28,7 +28,6 @@ import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicType import org.oxycblt.auxio.music.MusicType
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.fs.MimeType import org.oxycblt.auxio.music.fs.MimeType
import org.oxycblt.auxio.music.fs.Path
import org.oxycblt.auxio.music.fs.toAudioUri import org.oxycblt.auxio.music.fs.toAudioUri
import org.oxycblt.auxio.music.fs.toCoverUri import org.oxycblt.auxio.music.fs.toCoverUri
import org.oxycblt.auxio.music.info.Date import org.oxycblt.auxio.music.info.Date
@ -85,14 +84,10 @@ class SongImpl(
requireNotNull(rawSong.mediaStoreId) { "Invalid raw ${rawSong.fileName}: No id" } requireNotNull(rawSong.mediaStoreId) { "Invalid raw ${rawSong.fileName}: No id" }
.toAudioUri() .toAudioUri()
override val path = override val path =
Path( requireNotNull(rawSong.directory) { "Invalid raw ${rawSong.fileName}: No parent directory" }
name = .file(
requireNotNull(rawSong.fileName) { requireNotNull(rawSong.fileName) {
"Invalid raw ${rawSong.fileName}: No display name" "Invalid raw ${rawSong.fileName}: No display name"
},
parent =
requireNotNull(rawSong.directory) {
"Invalid raw ${rawSong.fileName}: No parent directory"
}) })
override val mimeType = override val mimeType =
MimeType( MimeType(
@ -247,11 +242,11 @@ class SongImpl(
* @return This instance upcasted to [Song]. * @return This instance upcasted to [Song].
*/ */
fun finalize(): Song { fun finalize(): Song {
checkNotNull(_album) { "Malformed song ${path.name}: No album" } checkNotNull(_album) { "Malformed song ${path}: No album" }
check(_artists.isNotEmpty()) { "Malformed song ${path.name}: No artists" } check(_artists.isNotEmpty()) { "Malformed song ${path}: No artists" }
check(_artists.size == rawArtists.size) { check(_artists.size == rawArtists.size) {
"Malformed song ${path.name}: Artist grouping mismatch" "Malformed song ${path}: Artist grouping mismatch"
} }
for (i in _artists.indices) { for (i in _artists.indices) {
// Non-destructively reorder the linked artists so that they align with // Non-destructively reorder the linked artists so that they align with
@ -262,10 +257,8 @@ class SongImpl(
_artists[i] = other _artists[i] = other
} }
check(_genres.isNotEmpty()) { "Malformed song ${path.name}: No genres" } check(_genres.isNotEmpty()) { "Malformed song ${path}: No genres" }
check(_genres.size == rawGenres.size) { check(_genres.size == rawGenres.size) { "Malformed song ${path}: Genre grouping mismatch" }
"Malformed song ${path.name}: Genre grouping mismatch"
}
for (i in _genres.indices) { for (i in _genres.indices) {
// Non-destructively reorder the linked genres so that they align with // Non-destructively reorder the linked genres so that they align with
// the genre ordering within the song metadata. // the genre ordering within the song metadata.
@ -519,6 +512,7 @@ class ArtistImpl(
return this return this
} }
} }
/** /**
* Library-backed implementation of [Genre]. * Library-backed implementation of [Genre].
* *

View file

@ -22,7 +22,7 @@ import java.util.UUID
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.fs.Directory import org.oxycblt.auxio.music.fs.Path
import org.oxycblt.auxio.music.info.Date import org.oxycblt.auxio.music.info.Date
import org.oxycblt.auxio.music.info.ReleaseType import org.oxycblt.auxio.music.info.ReleaseType
@ -44,7 +44,7 @@ data class RawSong(
/** @see Song.path */ /** @see Song.path */
var fileName: String? = null, var fileName: String? = null,
/** @see Song.path */ /** @see Song.path */
var directory: Directory? = null, var directory: Path? = null,
/** @see Song.size */ /** @see Song.size */
var size: Long? = null, var size: Long? = null,
/** @see Song.durationMs */ /** @see Song.durationMs */

View file

@ -16,13 +16,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.music.fs package org.oxycblt.auxio.music.dirs
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.databinding.ItemMusicDirBinding import org.oxycblt.auxio.databinding.ItemMusicDirBinding
import org.oxycblt.auxio.list.recycler.DialogRecyclerView import org.oxycblt.auxio.list.recycler.DialogRecyclerView
import org.oxycblt.auxio.music.fs.Path
import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -35,11 +36,11 @@ import org.oxycblt.auxio.util.logD
*/ */
class DirectoryAdapter(private val listener: Listener) : class DirectoryAdapter(private val listener: Listener) :
RecyclerView.Adapter<MusicDirViewHolder>() { RecyclerView.Adapter<MusicDirViewHolder>() {
private val _dirs = mutableListOf<Directory>() private val _dirs = mutableListOf<Path>()
/** /**
* The current list of [Directory]s, may not line up with [MusicDirectories] due to removals. * The current list of [SystemPath]s, may not line up with [MusicDirectories] due to removals.
*/ */
val dirs: List<Directory> = _dirs val dirs: List<Path> = _dirs
override fun getItemCount() = dirs.size override fun getItemCount() = dirs.size
@ -50,37 +51,37 @@ class DirectoryAdapter(private val listener: Listener) :
holder.bind(dirs[position], listener) holder.bind(dirs[position], listener)
/** /**
* Add a [Directory] to the end of the list. * Add a [Path] to the end of the list.
* *
* @param dir The [Directory] to add. * @param path The [Path] to add.
*/ */
fun add(dir: Directory) { fun add(path: Path) {
if (_dirs.contains(dir)) return if (_dirs.contains(path)) return
logD("Adding $dir") logD("Adding $path")
_dirs.add(dir) _dirs.add(path)
notifyItemInserted(_dirs.lastIndex) notifyItemInserted(_dirs.lastIndex)
} }
/** /**
* Add a list of [Directory] instances to the end of the list. * Add a list of [Path] instances to the end of the list.
* *
* @param dirs The [Directory] instances to add. * @param path The [Path] instances to add.
*/ */
fun addAll(dirs: List<Directory>) { fun addAll(path: List<Path>) {
logD("Adding ${dirs.size} directories") logD("Adding ${path.size} directories")
val oldLastIndex = dirs.lastIndex val oldLastIndex = path.lastIndex
_dirs.addAll(dirs) _dirs.addAll(path)
notifyItemRangeInserted(oldLastIndex, dirs.size) notifyItemRangeInserted(oldLastIndex, path.size)
} }
/** /**
* Remove a [Directory] from the list. * Remove a [Path] from the list.
* *
* @param dir The [Directory] to remove. Must exist in the list. * @param path The [Path] to remove. Must exist in the list.
*/ */
fun remove(dir: Directory) { fun remove(path: Path) {
logD("Removing $dir") logD("Removing $path")
val idx = _dirs.indexOf(dir) val idx = _dirs.indexOf(path)
_dirs.removeAt(idx) _dirs.removeAt(idx)
notifyItemRemoved(idx) notifyItemRemoved(idx)
} }
@ -88,7 +89,7 @@ class DirectoryAdapter(private val listener: Listener) :
/** A Listener for [DirectoryAdapter] interactions. */ /** A Listener for [DirectoryAdapter] interactions. */
interface Listener { interface Listener {
/** Called when the delete button on a directory item is clicked. */ /** Called when the delete button on a directory item is clicked. */
fun onRemoveDirectory(dir: Directory) fun onRemoveDirectory(dir: Path)
} }
} }
@ -102,12 +103,12 @@ class MusicDirViewHolder private constructor(private val binding: ItemMusicDirBi
/** /**
* Bind new data to this instance. * Bind new data to this instance.
* *
* @param dir The new [Directory] to bind. * @param path The new [Path] to bind.
* @param listener A [DirectoryAdapter.Listener] to bind interactions to. * @param listener A [DirectoryAdapter.Listener] to bind interactions to.
*/ */
fun bind(dir: Directory, listener: DirectoryAdapter.Listener) { fun bind(path: Path, listener: DirectoryAdapter.Listener) {
binding.dirPath.text = dir.resolveName(binding.context) binding.dirPath.text = path.resolve(binding.context)
binding.dirDelete.setOnClickListener { listener.onRemoveDirectory(dir) } binding.dirDelete.setOnClickListener { listener.onRemoveDirectory(path) }
} }
companion object { companion object {

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2023 Auxio Project
* DirectoryModule.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.music.dirs
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
interface DirectoryModule {
@Binds
fun documentTreePathFactory(factory: DocumentTreePathFactoryImpl): DocumentTreePathFactory
}

View file

@ -0,0 +1,104 @@
/*
* Copyright (c) 2023 Auxio Project
* DocumentTreePathFactory.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.music.dirs
import android.net.Uri
import android.provider.DocumentsContract
import java.io.File
import javax.inject.Inject
import org.oxycblt.auxio.music.fs.Components
import org.oxycblt.auxio.music.fs.Path
import org.oxycblt.auxio.music.fs.Volume
import org.oxycblt.auxio.music.fs.VolumeManager
/**
* A factory for parsing the reverse-engineered format of the URIs obtained from the document tree
* (i.e directory) folder.
*
* @author Alexander Capehart (OxygenCobalt)
*/
interface DocumentTreePathFactory {
/**
* Unpacks a document tree URI into a [Path] instance, using [deserializeDocumentTreePath].
*
* @param uri The document tree URI to unpack.
* @return The [Path] instance, or null if the URI could not be unpacked.
*/
fun unpackDocumentTreeUri(uri: Uri): Path?
/**
* Serializes a [Path] instance into a document tree URI format path.
*
* @param path The [Path] instance to serialize.
* @return The serialized path.
*/
fun serializeDocumentTreePath(path: Path): String
/**
* Deserializes a document tree URI format path into a [Path] instance.
*
* @param path The path to deserialize.
* @return The [Path] instance, or null if the path could not be deserialized.
*/
fun deserializeDocumentTreePath(path: String): Path?
}
class DocumentTreePathFactoryImpl @Inject constructor(private val volumeManager: VolumeManager) :
DocumentTreePathFactory {
override fun unpackDocumentTreeUri(uri: Uri): Path? {
// Convert the document tree URI into it's relative path form, which can then be
// parsed into a Directory instance.
val docUri =
DocumentsContract.buildDocumentUriUsingTree(
uri, DocumentsContract.getTreeDocumentId(uri))
val treeUri = DocumentsContract.getTreeDocumentId(docUri)
return deserializeDocumentTreePath(treeUri)
}
override fun serializeDocumentTreePath(path: Path): String =
when (val volume = path.volume) {
// The primary storage has a volume prefix of "primary", regardless
// of if it's internal or not.
is Volume.Internal -> "$DOCUMENT_URI_PRIMARY_NAME:${path.components}"
// Document tree URIs consist of a prefixed volume name followed by a relative path.
is Volume.External -> "${volume.id}:${path.components}"
}
override fun deserializeDocumentTreePath(path: String): Path? {
// Document tree URIs consist of a prefixed volume name followed by a relative path,
// delimited with a colon.
val split = path.split(File.pathSeparator, limit = 2)
val volume =
when (split[0]) {
// The primary storage has a volume prefix of "primary", regardless
// of if it's internal or not.
DOCUMENT_URI_PRIMARY_NAME -> volumeManager.getInternalVolume()
// Removable storage has a volume prefix of it's UUID, try to find it
// within StorageManager's volume list.
else ->
volumeManager.getVolumes().find { it is Volume.External && it.id == split[0] }
}
val relativePath = split.getOrNull(1) ?: return null
return Path(volume ?: return null, Components.parse(relativePath))
}
private companion object {
const val DOCUMENT_URI_PRIMARY_NAME = "primary"
}
}

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2023 Auxio Project
* MusicDirectories.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.music.dirs
import org.oxycblt.auxio.music.fs.Path
/**
* Represents the configuration for specific directories to filter to/from when loading music.
*
* @param dirs A list of [Directory] instances. How these are interpreted depends on [shouldInclude]
* @param shouldInclude True if the library should only load from the [Directory] instances, false
* if the library should not load from the [Directory] instances.
* @author Alexander Capehart (OxygenCobalt)
*/
data class MusicDirectories(val dirs: List<Path>, val shouldInclude: Boolean)

View file

@ -16,13 +16,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.music.fs package org.oxycblt.auxio.music.dirs
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.storage.StorageManager
import android.provider.DocumentsContract
import android.view.LayoutInflater import android.view.LayoutInflater
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
@ -35,8 +33,8 @@ import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogMusicDirsBinding import org.oxycblt.auxio.databinding.DialogMusicDirsBinding
import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.music.MusicSettings
import org.oxycblt.auxio.music.fs.Path
import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment
import org.oxycblt.auxio.util.getSystemServiceCompat
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.showToast
@ -50,7 +48,7 @@ class MusicDirsDialog :
ViewBindingMaterialDialogFragment<DialogMusicDirsBinding>(), DirectoryAdapter.Listener { ViewBindingMaterialDialogFragment<DialogMusicDirsBinding>(), DirectoryAdapter.Listener {
private val dirAdapter = DirectoryAdapter(this) private val dirAdapter = DirectoryAdapter(this)
private var openDocumentTreeLauncher: ActivityResultLauncher<Uri?>? = null private var openDocumentTreeLauncher: ActivityResultLauncher<Uri?>? = null
private var storageManager: StorageManager? = null @Inject lateinit var documentTreePathFactory: DocumentTreePathFactory
@Inject lateinit var musicSettings: MusicSettings @Inject lateinit var musicSettings: MusicSettings
override fun onCreateBinding(inflater: LayoutInflater) = override fun onCreateBinding(inflater: LayoutInflater) =
@ -70,10 +68,6 @@ class MusicDirsDialog :
} }
override fun onBindingCreated(binding: DialogMusicDirsBinding, savedInstanceState: Bundle?) { override fun onBindingCreated(binding: DialogMusicDirsBinding, savedInstanceState: Bundle?) {
val context = requireContext()
val storageManager =
context.getSystemServiceCompat(StorageManager::class).also { storageManager = it }
openDocumentTreeLauncher = openDocumentTreeLauncher =
registerForActivityResult( registerForActivityResult(
ActivityResultContracts.OpenDocumentTree(), ::addDocumentTreeUriToDirs) ActivityResultContracts.OpenDocumentTree(), ::addDocumentTreeUriToDirs)
@ -107,9 +101,8 @@ class MusicDirsDialog :
if (pendingDirs != null) { if (pendingDirs != null) {
dirs = dirs =
MusicDirectories( MusicDirectories(
pendingDirs.mapNotNull { pendingDirs.mapNotNull(
Directory.fromDocumentTreeUri(storageManager, it) documentTreePathFactory::deserializeDocumentTreePath),
},
savedInstanceState.getBoolean(KEY_PENDING_MODE)) savedInstanceState.getBoolean(KEY_PENDING_MODE))
} }
} }
@ -133,18 +126,18 @@ class MusicDirsDialog :
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
outState.putStringArrayList( outState.putStringArrayList(
KEY_PENDING_DIRS, ArrayList(dirAdapter.dirs.map { it.toString() })) KEY_PENDING_DIRS,
ArrayList(dirAdapter.dirs.map(documentTreePathFactory::serializeDocumentTreePath)))
outState.putBoolean(KEY_PENDING_MODE, isUiModeInclude(requireBinding())) outState.putBoolean(KEY_PENDING_MODE, isUiModeInclude(requireBinding()))
} }
override fun onDestroyBinding(binding: DialogMusicDirsBinding) { override fun onDestroyBinding(binding: DialogMusicDirsBinding) {
super.onDestroyBinding(binding) super.onDestroyBinding(binding)
storageManager = null
openDocumentTreeLauncher = null openDocumentTreeLauncher = null
binding.dirsRecycler.adapter = null binding.dirsRecycler.adapter = null
} }
override fun onRemoveDirectory(dir: Directory) { override fun onRemoveDirectory(dir: Path) {
dirAdapter.remove(dir) dirAdapter.remove(dir)
requireBinding().dirsEmpty.isVisible = dirAdapter.dirs.isEmpty() requireBinding().dirsEmpty.isVisible = dirAdapter.dirs.isEmpty()
} }
@ -162,15 +155,7 @@ class MusicDirsDialog :
return return
} }
// Convert the document tree URI into it's relative path form, which can then be val dir = documentTreePathFactory.unpackDocumentTreeUri(uri)
// parsed into a Directory instance.
val docUri =
DocumentsContract.buildDocumentUriUsingTree(
uri, DocumentsContract.getTreeDocumentId(uri))
val treeUri = DocumentsContract.getTreeDocumentId(docUri)
val dir =
Directory.fromDocumentTreeUri(
requireNotNull(storageManager) { "StorageManager was not available" }, treeUri)
if (dir != null) { if (dir != null) {
dirAdapter.add(dir) dirAdapter.add(dir)

View file

@ -24,119 +24,174 @@ import android.os.storage.StorageManager
import android.os.storage.StorageVolume import android.os.storage.StorageVolume
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import java.io.File import java.io.File
import javax.inject.Inject
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.logD
/** /**
* A full absolute path to a file. Only intended for display purposes. For accessing files, URIs are * An abstraction of an android file system path, including the volume and relative path.
* preferred in all cases due to scoped storage limitations.
* *
* @param name The name of the file. * @param volume The volume that the path is on.
* @param parent The parent [Directory] of the file. * @param components The components of the path of the file, relative to the root of the volume.
* @author Alexander Capehart (OxygenCobalt)
*/ */
data class Path(val name: String, val parent: Directory) data class Path(
val volume: Volume,
val components: Components,
) {
/** The name of the file/directory. */
val name: String?
get() = components.name
/** /** The parent directory of the path, or itself if it's the root path. */
* A volume-aware relative path to a directory. val directory: Path
* get() = Path(volume, components.parent())
* @param volume The [StorageVolume] that the [Directory] is contained in.
* @param relativePath The relative path from within the [StorageVolume] to the [Directory]. override fun toString() = "Path(storageVolume=$volume, components=$components)"
* @author Alexander Capehart (OxygenCobalt)
*/
class Directory private constructor(val volume: StorageVolume, val relativePath: String) {
/**
* Resolve the [Directory] instance into a human-readable path name.
*
* @param context [Context] required to obtain volume descriptions.
* @return A human-readable path.
* @see StorageVolume.getDescription
*/
fun resolveName(context: Context) =
context.getString(R.string.fmt_path, volume.getDescriptionCompat(context), relativePath)
/** /**
* Converts this [Directory] instance into an opaque document tree path. This is a huge * Transforms this [Path] into a "file" of the given name that's within the "directory"
* violation of the document tree URI contract, but it's also the only one can sensibly work * represented by the current path. Ex. "/storage/emulated/0/Music" ->
* with these uris in the UI, and it doesn't exactly matter since we never write or read to * "/storage/emulated/0/Music/file.mp3"
* directory.
* *
* @return A URI [String] abiding by the document tree specification, or null if the [Directory] * @param fileName The name of the file to append to the path.
* is not valid. * @return The new [Path] instance.
*/ */
fun toDocumentTreeUri() = fun file(fileName: String) = Path(volume, components.child(fileName))
// Document tree URIs consist of a prefixed volume name followed by a relative path.
if (volume.isInternalCompat) {
// The primary storage has a volume prefix of "primary", regardless
// of if it's internal or not.
"$DOCUMENT_URI_PRIMARY_NAME:$relativePath"
} else {
// Removable storage has a volume prefix of it's UUID.
volume.uuidCompat?.let { uuid -> "$uuid:$relativePath" }
}
override fun hashCode(): Int { /**
var result = volume.hashCode() * Resolves the [Path] in a human-readable format.
result = 31 * result + relativePath.hashCode() *
return result * @param context [Context] required to obtain human-readable strings.
} */
fun resolve(context: Context) = "${volume.resolveName(context)}/$components"
}
override fun equals(other: Any?) = sealed interface Volume {
other is Directory && other.volume == volume && other.relativePath == relativePath /** The name of the volume as it appears in MediaStore. */
val mediaStoreName: String?
companion object { /**
/** The name given to the internal volume when in a document tree URI. */ * The components of the path to the volume, relative from the system root. Should not be used
private const val DOCUMENT_URI_PRIMARY_NAME = "primary" * except for compatibility purposes.
*/
val components: Components?
/** /** Resolves the name of the volume in a human-readable format. */
* Create a new directory instance from the given components. fun resolveName(context: Context): String
*
* @param volume The [StorageVolume] that the [Directory] is contained in.
* @param relativePath The relative path from within the [StorageVolume] to the [Directory].
* Will be stripped of any trailing separators for a consistent internal representation.
* @return A new [Directory] created from the components.
*/
fun from(volume: StorageVolume, relativePath: String) =
Directory(
volume, relativePath.removePrefix(File.separator).removeSuffix(File.separator))
/** /** A volume representing the device's internal storage. */
* Create a new directory from a document tree URI. This is a huge violation of the document interface Internal : Volume
* tree URI contract, but it's also the only one can sensibly work with these uris in the
* UI, and it doesn't exactly matter since we never write or read directory. /** A volume representing an external storage device, identified by a UUID. */
* interface External : Volume {
* @param storageManager [StorageManager] in order to obtain the [StorageVolume] specified /** The UUID of the volume. */
* in the given URI. val id: String?
* @param uri The URI string to parse into a [Directory].
* @return A new [Directory] parsed from the URI, or null if the URI is not valid.
*/
fun fromDocumentTreeUri(storageManager: StorageManager, uri: String): Directory? {
// Document tree URIs consist of a prefixed volume name followed by a relative path,
// delimited with a colon.
val split = uri.split(File.pathSeparator, limit = 2)
val volume =
when (split[0]) {
// The primary storage has a volume prefix of "primary", regardless
// of if it's internal or not.
DOCUMENT_URI_PRIMARY_NAME -> storageManager.primaryStorageVolumeCompat
// Removable storage has a volume prefix of it's UUID, try to find it
// within StorageManager's volume list.
else -> storageManager.storageVolumesCompat.find { it.uuidCompat == split[0] }
}
val relativePath = split.getOrNull(1)
return from(volume ?: return null, relativePath ?: return null)
}
} }
} }
/** /**
* Represents the configuration for specific directories to filter to/from when loading music. * The components of a path. This allows the path to be manipulated without having tp handle
* separator parsing.
* *
* @param dirs A list of [Directory] instances. How these are interpreted depends on [shouldInclude] * @param components The components of the path.
* @param shouldInclude True if the library should only load from the [Directory] instances, false
* if the library should not load from the [Directory] instances.
* @author Alexander Capehart (OxygenCobalt)
*/ */
data class MusicDirectories(val dirs: List<Directory>, val shouldInclude: Boolean) @JvmInline
value class Components private constructor(val components: List<String>) {
/** The name of the file/directory. */
val name: String?
get() = components.lastOrNull()
override fun toString() = components.joinToString(File.separator)
/**
* Returns a new [Components] instance with the last element of the path removed as a "parent"
* element of the original instance.
*
* @return The new [Components] instance, or the original instance if it's the root path.
*/
fun parent() = Components(components.dropLast(1))
/**
* Returns a new [Components] instance with the given name appended to the end of the path as a
* "child" element of the original instance.
*
* @param name The name of the file/directory to append to the path.
*/
fun child(name: String) =
if (name.isNotEmpty()) {
Components(components + name.trimSlashes()).also { logD(it.components) }
} else {
this
}
companion object {
/**
* Parses a path string into a [Components] instance by the system path separator.
*
* @param path The path string to parse.
* @return The [Components] instance.
*/
fun parse(path: String) =
Components(path.trimSlashes().split(File.separatorChar).filter { it.isNotEmpty() })
private fun String.trimSlashes() = trimStart(File.separatorChar).trimEnd(File.separatorChar)
}
}
/** A wrapper around [StorageManager] that provides instances of the [Volume] interface. */
interface VolumeManager {
/**
* The internal storage volume of the device.
*
* @see StorageManager.getPrimaryStorageVolume
*/
fun getInternalVolume(): Volume.Internal
/**
* The list of [Volume]s currently recognized by [StorageManager].
*
* @see StorageManager.getStorageVolumes
*/
fun getVolumes(): List<Volume>
}
class VolumeManagerImpl @Inject constructor(private val storageManager: StorageManager) :
VolumeManager {
override fun getInternalVolume(): Volume.Internal =
InternalVolumeImpl(storageManager.primaryStorageVolume)
override fun getVolumes() =
storageManager.storageVolumesCompat.map {
if (it.isInternalCompat) {
InternalVolumeImpl(it)
} else {
ExternalVolumeImpl(it)
}
}
private class InternalVolumeImpl(val storageVolume: StorageVolume) : Volume.Internal {
override val mediaStoreName
get() = storageVolume.mediaStoreVolumeNameCompat
override val components
get() = storageVolume.directoryCompat?.let(Components::parse)
override fun resolveName(context: Context) = storageVolume.getDescriptionCompat(context)
}
private class ExternalVolumeImpl(val storageVolume: StorageVolume) : Volume.External {
override val id
get() = storageVolume.uuidCompat
override val mediaStoreName
get() = storageVolume.mediaStoreVolumeNameCompat
override val components
get() = storageVolume.directoryCompat?.let(Components::parse)
override fun resolveName(context: Context) = storageVolume.getDescriptionCompat(context)
}
}
/** /**
* A mime type of a file. Only intended for display. * A mime type of a file. Only intended for display.

View file

@ -19,16 +19,22 @@
package org.oxycblt.auxio.music.fs package org.oxycblt.auxio.music.fs
import android.content.Context import android.content.Context
import android.os.storage.StorageManager
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import org.oxycblt.auxio.util.getSystemServiceCompat
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
class FsModule { class FsModule {
@Provides @Provides
fun mediaStoreExtractor(@ApplicationContext context: Context) = fun volumeManager(@ApplicationContext context: Context): VolumeManager =
MediaStoreExtractor.from(context) VolumeManagerImpl(context.getSystemServiceCompat(StorageManager::class))
@Provides
fun mediaStoreExtractor(@ApplicationContext context: Context, volumeManager: VolumeManager) =
MediaStoreExtractor.from(context, volumeManager)
} }

View file

@ -21,7 +21,6 @@ package org.oxycblt.auxio.music.fs
import android.content.Context import android.content.Context
import android.database.Cursor import android.database.Cursor
import android.os.Build import android.os.Build
import android.os.storage.StorageManager
import android.provider.MediaStore import android.provider.MediaStore
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.database.getIntOrNull import androidx.core.database.getIntOrNull
@ -31,10 +30,10 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import org.oxycblt.auxio.music.cache.Cache import org.oxycblt.auxio.music.cache.Cache
import org.oxycblt.auxio.music.device.RawSong import org.oxycblt.auxio.music.device.RawSong
import org.oxycblt.auxio.music.dirs.MusicDirectories
import org.oxycblt.auxio.music.info.Date import org.oxycblt.auxio.music.info.Date
import org.oxycblt.auxio.music.metadata.parseId3v2PositionField import org.oxycblt.auxio.music.metadata.parseId3v2PositionField
import org.oxycblt.auxio.music.metadata.transformPositionField import org.oxycblt.auxio.music.metadata.transformPositionField
import org.oxycblt.auxio.util.getSystemServiceCompat
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.sendWithTimeout import org.oxycblt.auxio.util.sendWithTimeout
@ -93,13 +92,16 @@ interface MediaStoreExtractor {
* Create a framework-backed instance. * Create a framework-backed instance.
* *
* @param context [Context] required. * @param context [Context] required.
* @param volumeManager [VolumeManager] required.
* @return A new [MediaStoreExtractor] that will work best on the device's API level. * @return A new [MediaStoreExtractor] that will work best on the device's API level.
*/ */
fun from(context: Context): MediaStoreExtractor = fun from(context: Context, volumeManager: VolumeManager): MediaStoreExtractor =
when { when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> Api30MediaStoreExtractor(context) Build.VERSION.SDK_INT >= Build.VERSION_CODES.R ->
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> Api29MediaStoreExtractor(context) Api30MediaStoreExtractor(context, volumeManager)
else -> Api21MediaStoreExtractor(context) Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ->
Api29MediaStoreExtractor(context, volumeManager)
else -> Api21MediaStoreExtractor(context, volumeManager)
} }
} }
} }
@ -249,15 +251,15 @@ private abstract class BaseMediaStoreExtractor(protected val context: Context) :
protected abstract val dirSelectorTemplate: String protected abstract val dirSelectorTemplate: String
/** /**
* Add a [Directory] to the given list of projection selector arguments. * Add a [SystemPath] to the given list of projection selector arguments.
* *
* @param dir The [Directory] to add. * @param path The [SystemPath] to add.
* @param args The destination list to append selector arguments to that are analogous to the * @param args The destination list to append selector arguments to that are analogous to the
* given [Directory]. * given [SystemPath].
* @return true if the [Directory] was added, false otherwise. * @return true if the [SystemPath] was added, false otherwise.
* @see dirSelectorTemplate * @see dirSelectorTemplate
*/ */
protected abstract fun addDirToSelector(dir: Directory, args: MutableList<String>): Boolean protected abstract fun addDirToSelector(path: Path, args: MutableList<String>): Boolean
protected abstract fun wrapQuery( protected abstract fun wrapQuery(
cursor: Cursor, cursor: Cursor,
@ -362,7 +364,8 @@ private abstract class BaseMediaStoreExtractor(protected val context: Context) :
// Note: The separation between version-specific backends may not be the cleanest. To preserve // Note: The separation between version-specific backends may not be the cleanest. To preserve
// speed, we only want to add redundancy on known issues, not with possible issues. // speed, we only want to add redundancy on known issues, not with possible issues.
private class Api21MediaStoreExtractor(context: Context) : BaseMediaStoreExtractor(context) { private class Api21MediaStoreExtractor(context: Context, private val volumeManager: VolumeManager) :
BaseMediaStoreExtractor(context) {
override val projection: Array<String> override val projection: Array<String>
get() = get() =
super.projection + super.projection +
@ -378,28 +381,27 @@ private class Api21MediaStoreExtractor(context: Context) : BaseMediaStoreExtract
override val dirSelectorTemplate: String override val dirSelectorTemplate: String
get() = "${MediaStore.Audio.Media.DATA} LIKE ?" get() = "${MediaStore.Audio.Media.DATA} LIKE ?"
override fun addDirToSelector(dir: Directory, args: MutableList<String>): Boolean { override fun addDirToSelector(path: Path, args: MutableList<String>): Boolean {
// "%" signifies to accept any DATA value that begins with the Directory's path, // "%" signifies to accept any DATA value that begins with the Directory's path,
// thus recursively filtering all files in the directory. // thus recursively filtering all files in the directory.
args.add("${dir.volume.directoryCompat ?: return false}/${dir.relativePath}%") args.add("${path.volume.components ?: return false}${path.components}%")
return true return true
} }
override fun wrapQuery( override fun wrapQuery(
cursor: Cursor, cursor: Cursor,
genreNamesMap: Map<Long, String>, genreNamesMap: Map<Long, String>,
): MediaStoreExtractor.Query = ): MediaStoreExtractor.Query = Query(cursor, genreNamesMap, volumeManager)
Query(cursor, genreNamesMap, context.getSystemServiceCompat(StorageManager::class))
private class Query( private class Query(
cursor: Cursor, cursor: Cursor,
genreNamesMap: Map<Long, String>, genreNamesMap: Map<Long, String>,
storageManager: StorageManager volumeManager: VolumeManager
) : BaseMediaStoreExtractor.Query(cursor, genreNamesMap) { ) : BaseMediaStoreExtractor.Query(cursor, genreNamesMap) {
// Set up cursor indices for later use. // Set up cursor indices for later use.
private val trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK) private val trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK)
private val dataIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DATA) private val dataIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DATA)
private val volumes = storageManager.storageVolumesCompat private val volumes = volumeManager.getVolumes()
override fun populateFileInfo(rawSong: RawSong) { override fun populateFileInfo(rawSong: RawSong) {
super.populateFileInfo(rawSong) super.populateFileInfo(rawSong)
@ -417,10 +419,10 @@ private class Api21MediaStoreExtractor(context: Context) : BaseMediaStoreExtract
// the Directory we will use. // the Directory we will use.
val rawPath = data.substringBeforeLast(File.separatorChar) val rawPath = data.substringBeforeLast(File.separatorChar)
for (volume in volumes) { for (volume in volumes) {
val volumePath = volume.directoryCompat ?: continue val volumePath = (volume.components ?: continue).toString()
val strippedPath = rawPath.removePrefix(volumePath) val strippedPath = rawPath.removePrefix(volumePath)
if (strippedPath != rawPath) { if (strippedPath != rawPath) {
rawSong.directory = Directory.from(volume, strippedPath) rawSong.directory = Path(volume, Components.parse(strippedPath))
break break
} }
} }
@ -466,26 +468,26 @@ private abstract class BaseApi29MediaStoreExtractor(context: Context) :
"(${MediaStore.Audio.AudioColumns.VOLUME_NAME} LIKE ? " + "(${MediaStore.Audio.AudioColumns.VOLUME_NAME} LIKE ? " +
"AND ${MediaStore.Audio.AudioColumns.RELATIVE_PATH} LIKE ?)" "AND ${MediaStore.Audio.AudioColumns.RELATIVE_PATH} LIKE ?)"
override fun addDirToSelector(dir: Directory, args: MutableList<String>): Boolean { override fun addDirToSelector(path: Path, args: MutableList<String>): Boolean {
// MediaStore uses a different naming scheme for it's volume column convert this // MediaStore uses a different naming scheme for it's volume column convert this
// directory's volume to it. // directory's volume to it.
args.add(dir.volume.mediaStoreVolumeNameCompat ?: return false) args.add(path.volume.mediaStoreName ?: return false)
// "%" signifies to accept any DATA value that begins with the Directory's path, // "%" signifies to accept any DATA value that begins with the Directory's path,
// thus recursively filtering all files in the directory. // thus recursively filtering all files in the directory.
args.add("${dir.relativePath}%") args.add("${path.components}%")
return true return true
} }
abstract class Query( abstract class Query(
cursor: Cursor, cursor: Cursor,
genreNamesMap: Map<Long, String>, genreNamesMap: Map<Long, String>,
storageManager: StorageManager private val volumeManager: VolumeManager
) : BaseMediaStoreExtractor.Query(cursor, genreNamesMap) { ) : BaseMediaStoreExtractor.Query(cursor, genreNamesMap) {
private val volumeIndex = private val volumeIndex =
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.VOLUME_NAME) cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.VOLUME_NAME)
private val relativePathIndex = private val relativePathIndex =
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.RELATIVE_PATH) cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.RELATIVE_PATH)
private val volumes = storageManager.storageVolumesCompat private val volumes = volumeManager.getVolumes()
final override fun populateFileInfo(rawSong: RawSong) { final override fun populateFileInfo(rawSong: RawSong) {
super.populateFileInfo(rawSong) super.populateFileInfo(rawSong)
@ -493,9 +495,9 @@ private abstract class BaseApi29MediaStoreExtractor(context: Context) :
// This is combined with the plain relative path column to create the directory. // This is combined with the plain relative path column to create the directory.
val volumeName = cursor.getString(volumeIndex) val volumeName = cursor.getString(volumeIndex)
val relativePath = cursor.getString(relativePathIndex) val relativePath = cursor.getString(relativePathIndex)
val volume = volumes.find { it.mediaStoreVolumeNameCompat == volumeName } val volume = volumes.find { it.mediaStoreName == volumeName }
if (volume != null) { if (volume != null) {
rawSong.directory = Directory.from(volume, relativePath) rawSong.directory = Path(volume, Components.parse(relativePath))
} }
} }
} }
@ -509,7 +511,8 @@ private abstract class BaseApi29MediaStoreExtractor(context: Context) :
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
@RequiresApi(Build.VERSION_CODES.Q) @RequiresApi(Build.VERSION_CODES.Q)
private class Api29MediaStoreExtractor(context: Context) : BaseApi29MediaStoreExtractor(context) { private class Api29MediaStoreExtractor(context: Context, private val volumeManager: VolumeManager) :
BaseApi29MediaStoreExtractor(context) {
override val projection: Array<String> override val projection: Array<String>
get() = super.projection + arrayOf(MediaStore.Audio.AudioColumns.TRACK) get() = super.projection + arrayOf(MediaStore.Audio.AudioColumns.TRACK)
@ -517,14 +520,13 @@ private class Api29MediaStoreExtractor(context: Context) : BaseApi29MediaStoreEx
override fun wrapQuery( override fun wrapQuery(
cursor: Cursor, cursor: Cursor,
genreNamesMap: Map<Long, String> genreNamesMap: Map<Long, String>
): MediaStoreExtractor.Query = ): MediaStoreExtractor.Query = Query(cursor, genreNamesMap, volumeManager)
Query(cursor, genreNamesMap, context.getSystemServiceCompat(StorageManager::class))
private class Query( private class Query(
cursor: Cursor, cursor: Cursor,
genreNamesMap: Map<Long, String>, genreNamesMap: Map<Long, String>,
storageManager: StorageManager volumeManager: VolumeManager
) : BaseApi29MediaStoreExtractor.Query(cursor, genreNamesMap, storageManager) { ) : BaseApi29MediaStoreExtractor.Query(cursor, genreNamesMap, volumeManager) {
private val trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK) private val trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK)
override fun populateTags(rawSong: RawSong) { override fun populateTags(rawSong: RawSong) {
@ -549,7 +551,8 @@ private class Api29MediaStoreExtractor(context: Context) : BaseApi29MediaStoreEx
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
@RequiresApi(Build.VERSION_CODES.R) @RequiresApi(Build.VERSION_CODES.R)
private class Api30MediaStoreExtractor(context: Context) : BaseApi29MediaStoreExtractor(context) { private class Api30MediaStoreExtractor(context: Context, private val volumeManager: VolumeManager) :
BaseApi29MediaStoreExtractor(context) {
override val projection: Array<String> override val projection: Array<String>
get() = get() =
super.projection + super.projection +
@ -562,14 +565,13 @@ private class Api30MediaStoreExtractor(context: Context) : BaseApi29MediaStoreEx
override fun wrapQuery( override fun wrapQuery(
cursor: Cursor, cursor: Cursor,
genreNamesMap: Map<Long, String> genreNamesMap: Map<Long, String>
): MediaStoreExtractor.Query = ): MediaStoreExtractor.Query = Query(cursor, genreNamesMap, volumeManager)
Query(cursor, genreNamesMap, context.getSystemServiceCompat(StorageManager::class))
private class Query( private class Query(
cursor: Cursor, cursor: Cursor,
genreNamesMap: Map<Long, String>, genreNamesMap: Map<Long, String>,
storageManager: StorageManager volumeManager: VolumeManager
) : BaseApi29MediaStoreExtractor.Query(cursor, genreNamesMap, storageManager) { ) : BaseApi29MediaStoreExtractor.Query(cursor, genreNamesMap, volumeManager) {
private val trackIndex = private val trackIndex =
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.CD_TRACK_NUMBER) cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.CD_TRACK_NUMBER)
private val discIndex = private val discIndex =

View file

@ -71,7 +71,7 @@ class SearchEngineImpl @Inject constructor(@ApplicationContext private val conte
return SearchEngine.Items( return SearchEngine.Items(
songs = songs =
items.songs?.searchListImpl(query) { q, song -> items.songs?.searchListImpl(query) { q, song ->
song.path.name.contains(q, ignoreCase = true) song.path.name?.contains(q, ignoreCase = true) == true
}, },
albums = items.albums?.searchListImpl(query), albums = items.albums?.searchListImpl(query),
artists = items.artists?.searchListImpl(query), artists = items.artists?.searchListImpl(query),

View file

@ -9,7 +9,7 @@
android:paddingEnd="@dimen/spacing_medium" android:paddingEnd="@dimen/spacing_medium"
android:paddingTop="@dimen/spacing_small" android:paddingTop="@dimen/spacing_small"
android:paddingBottom="@dimen/spacing_small" android:paddingBottom="@dimen/spacing_small"
tools:hint="@string/lbl_file_name" tools:hint="@string/lbl_path"
app:expandedHintEnabled="false"> app:expandedHintEnabled="false">
<org.oxycblt.auxio.detail.ReadOnlyTextInput <org.oxycblt.auxio.detail.ReadOnlyTextInput

View file

@ -91,7 +91,7 @@
tools:layout="@layout/dialog_pre_amp" /> tools:layout="@layout/dialog_pre_amp" />
<dialog <dialog
android:id="@+id/music_dirs_dialog" android:id="@+id/music_dirs_dialog"
android:name="org.oxycblt.auxio.music.fs.MusicDirsDialog" android:name="org.oxycblt.auxio.music.dirs.MusicDirsDialog"
android:label="music_dirs_dialog" android:label="music_dirs_dialog"
tools:layout="@layout/dialog_music_dirs" /> tools:layout="@layout/dialog_music_dirs" />
<dialog <dialog

View file

@ -143,10 +143,8 @@
<string name="lbl_cancel">الغاء</string> <string name="lbl_cancel">الغاء</string>
<string name="lbl_format">التنسيق</string> <string name="lbl_format">التنسيق</string>
<string name="lbl_size">الحجم</string> <string name="lbl_size">الحجم</string>
<string name="lbl_relative_path">المسار</string>
<string name="lbl_library_counts">إحصائيات المكتبة</string> <string name="lbl_library_counts">إحصائيات المكتبة</string>
<string name="lbl_bitrate">معدل البت</string> <string name="lbl_bitrate">معدل البت</string>
<string name="lbl_file_name">اسم الملف</string>
<string name="lbl_compilation_live">تجميع مباشر</string> <string name="lbl_compilation_live">تجميع مباشر</string>
<string name="lbl_compilation_remix">تجميعات</string> <string name="lbl_compilation_remix">تجميعات</string>
<string name="lbl_props">خصائص الاغنية</string> <string name="lbl_props">خصائص الاغنية</string>

View file

@ -32,7 +32,6 @@
<string name="lbl_artist_details">اذهب للفنان</string> <string name="lbl_artist_details">اذهب للفنان</string>
<string name="lng_widget">عرض والتحكم في تشغيل الموسيقى</string> <string name="lng_widget">عرض والتحكم في تشغيل الموسيقى</string>
<string name="lbl_shuffle_shortcut_short">خلط</string> <string name="lbl_shuffle_shortcut_short">خلط</string>
<string name="lbl_file_name">اسم الملف</string>
<string name="lbl_shuffle_shortcut_long">خلط الكل</string> <string name="lbl_shuffle_shortcut_long">خلط الكل</string>
<string name="lbl_cancel">إلغاء</string> <string name="lbl_cancel">إلغاء</string>
<string name="lbl_save">حفظ</string> <string name="lbl_save">حفظ</string>

View file

@ -83,10 +83,8 @@
<string name="lbl_queue">Чарга</string> <string name="lbl_queue">Чарга</string>
<string name="lbl_album_details">Перайсці да альбома</string> <string name="lbl_album_details">Перайсці да альбома</string>
<string name="lbl_artist_details">Перайсці да выканаўцы</string> <string name="lbl_artist_details">Перайсці да выканаўцы</string>
<string name="lbl_file_name">Імя файла</string>
<string name="lbl_song_detail">Праглядзіце ўласцівасці</string> <string name="lbl_song_detail">Праглядзіце ўласцівасці</string>
<string name="lbl_props">Уласцівасці песні</string> <string name="lbl_props">Уласцівасці песні</string>
<string name="lbl_relative_path">Бацькоўскі шлях</string>
<string name="lbl_format">Фармат</string> <string name="lbl_format">Фармат</string>
<string name="lbl_shuffle_shortcut_long">Перамяшаць усё</string> <string name="lbl_shuffle_shortcut_long">Перамяшаць усё</string>
<string name="lbl_bitrate">Бітрэйт</string> <string name="lbl_bitrate">Бітрэйт</string>

View file

@ -185,8 +185,6 @@
<string name="set_play_song_none">Přehrát ze zobrazené položky</string> <string name="set_play_song_none">Přehrát ze zobrazené položky</string>
<string name="lbl_song_detail">Zobrazit vlastnosti</string> <string name="lbl_song_detail">Zobrazit vlastnosti</string>
<string name="lbl_props">Vlastnosti skladby</string> <string name="lbl_props">Vlastnosti skladby</string>
<string name="lbl_file_name">Název souboru</string>
<string name="lbl_relative_path">Nadřazená cesta</string>
<string name="lbl_format">Formát</string> <string name="lbl_format">Formát</string>
<string name="lbl_size">Velikost</string> <string name="lbl_size">Velikost</string>
<string name="lbl_bitrate">Přenosová rychlost</string> <string name="lbl_bitrate">Přenosová rychlost</string>

View file

@ -179,8 +179,6 @@
<string name="lbl_sample_rate">Abtastrate</string> <string name="lbl_sample_rate">Abtastrate</string>
<string name="lbl_song_detail">Eigenschaften ansehen</string> <string name="lbl_song_detail">Eigenschaften ansehen</string>
<string name="lbl_props">Lied-Eigenschaften</string> <string name="lbl_props">Lied-Eigenschaften</string>
<string name="lbl_file_name">Dateiname</string>
<string name="lbl_relative_path">Elternpfad</string>
<string name="lbl_format">Format</string> <string name="lbl_format">Format</string>
<string name="lbl_size">Größe</string> <string name="lbl_size">Größe</string>
<string name="lbl_bitrate">Bitrate</string> <string name="lbl_bitrate">Bitrate</string>

View file

@ -80,7 +80,6 @@
<string name="fmt_bitrate">%d kbps</string> <string name="fmt_bitrate">%d kbps</string>
<string name="lbl_add">Πρόσθεση</string> <string name="lbl_add">Πρόσθεση</string>
<string name="lbl_props">Ιδιότητες τραγουδιού</string> <string name="lbl_props">Ιδιότητες τραγουδιού</string>
<string name="lbl_file_name">Όνομα αρχείου</string>
<string name="lbl_song_detail">Προβολή Ιδιοτήτων</string> <string name="lbl_song_detail">Προβολή Ιδιοτήτων</string>
<string name="lbl_library_counts">Στατιστικά συλλογής</string> <string name="lbl_library_counts">Στατιστικά συλλογής</string>
<string name="lbl_album_live">Ζωντανό άλμπουμ</string> <string name="lbl_album_live">Ζωντανό άλμπουμ</string>

View file

@ -194,7 +194,6 @@
<string name="lbl_mixtapes">Mixtapes (recopilación de canciones)</string> <string name="lbl_mixtapes">Mixtapes (recopilación de canciones)</string>
<string name="lbl_mixtape">Mixtape (recopilación de canciones)</string> <string name="lbl_mixtape">Mixtape (recopilación de canciones)</string>
<string name="lbl_remix_group">Remezclas</string> <string name="lbl_remix_group">Remezclas</string>
<string name="lbl_file_name">Nombre de archivo</string>
<string name="set_headset_autoplay_desc">Siempre empezar la reproducción cuando se conecten auriculares (puede no funcionar en todos los dispositivos)</string> <string name="set_headset_autoplay_desc">Siempre empezar la reproducción cuando se conecten auriculares (puede no funcionar en todos los dispositivos)</string>
<string name="set_pre_amp">Pre-amp ReplayGain</string> <string name="set_pre_amp">Pre-amp ReplayGain</string>
<string name="set_pre_amp_desc">El pre-amp se aplica al ajuste existente durante la reproducción</string> <string name="set_pre_amp_desc">El pre-amp se aplica al ajuste existente durante la reproducción</string>
@ -215,7 +214,6 @@
<string name="lbl_single_remix">Single remix</string> <string name="lbl_single_remix">Single remix</string>
<string name="lbl_compilations">Compilaciones</string> <string name="lbl_compilations">Compilaciones</string>
<string name="lbl_ep_remix">EP de remixes</string> <string name="lbl_ep_remix">EP de remixes</string>
<string name="lbl_relative_path">Directorio superior</string>
<string name="set_wipe_desc">Eliminar el estado de reproducción guardado previamente (si existe)</string> <string name="set_wipe_desc">Eliminar el estado de reproducción guardado previamente (si existe)</string>
<string name="desc_queue_bar">Abrir la cola</string> <string name="desc_queue_bar">Abrir la cola</string>
<string name="lbl_genre">Género</string> <string name="lbl_genre">Género</string>

View file

@ -43,8 +43,6 @@
<string name="lbl_album_details">Siirry albumiin</string> <string name="lbl_album_details">Siirry albumiin</string>
<string name="lbl_song_detail">Näytä ominaisuudet</string> <string name="lbl_song_detail">Näytä ominaisuudet</string>
<string name="lbl_props">Kappaleen ominaisuudet</string> <string name="lbl_props">Kappaleen ominaisuudet</string>
<string name="lbl_file_name">Tiedostonimi</string>
<string name="lbl_relative_path">Ylätason polku</string>
<string name="lbl_format">Muoto</string> <string name="lbl_format">Muoto</string>
<string name="lbl_size">Koko</string> <string name="lbl_size">Koko</string>
<string name="lbl_bitrate">Bittitaajuus</string> <string name="lbl_bitrate">Bittitaajuus</string>

View file

@ -46,7 +46,6 @@
<string name="lbl_album_details">Puntahan ang album</string> <string name="lbl_album_details">Puntahan ang album</string>
<string name="lbl_song_detail">Tignan ang katangian</string> <string name="lbl_song_detail">Tignan ang katangian</string>
<string name="lbl_props">Katangian ng kanta</string> <string name="lbl_props">Katangian ng kanta</string>
<string name="lbl_file_name">Pangalan ng file</string>
<string name="lbl_format">Pormat</string> <string name="lbl_format">Pormat</string>
<string name="lbl_size">Laki</string> <string name="lbl_size">Laki</string>
<string name="lbl_bitrate">Tulin ng mga bit</string> <string name="lbl_bitrate">Tulin ng mga bit</string>

View file

@ -82,8 +82,6 @@
<string name="lbl_bitrate">Débit binaire</string> <string name="lbl_bitrate">Débit binaire</string>
<string name="set_notif_action">Utiliser une autre action de notification</string> <string name="set_notif_action">Utiliser une autre action de notification</string>
<string name="lbl_sample_rate">Taux d\'échantillonnage</string> <string name="lbl_sample_rate">Taux d\'échantillonnage</string>
<string name="lbl_relative_path">Chemin parent</string>
<string name="lbl_file_name">Nom du fichier</string>
<string name="lbl_shuffle_shortcut_long">Tout mélanger</string> <string name="lbl_shuffle_shortcut_long">Tout mélanger</string>
<string name="lbl_cancel">Annuler</string> <string name="lbl_cancel">Annuler</string>
<string name="lbl_save">Enregistrer</string> <string name="lbl_save">Enregistrer</string>

View file

@ -53,11 +53,9 @@
<string name="lbl_queue_add">Engadir á cola</string> <string name="lbl_queue_add">Engadir á cola</string>
<string name="set_exclude_non_music">Excluir o que non é música</string> <string name="set_exclude_non_music">Excluir o que non é música</string>
<string name="lbl_artist_details">Ir ao artista</string> <string name="lbl_artist_details">Ir ao artista</string>
<string name="lbl_file_name">Nome do arquivo</string>
<string name="lbl_shuffle_shortcut_short">Mesturar</string> <string name="lbl_shuffle_shortcut_short">Mesturar</string>
<string name="lbl_shuffle_shortcut_long">Mesturar todo</string> <string name="lbl_shuffle_shortcut_long">Mesturar todo</string>
<string name="lbl_reset">Restablecer</string> <string name="lbl_reset">Restablecer</string>
<string name="lbl_relative_path">Directorio superior</string>
<string name="lbl_format">Formato</string> <string name="lbl_format">Formato</string>
<string name="lbl_size">Tamaño</string> <string name="lbl_size">Tamaño</string>
<string name="lbl_bitrate">Tasa de bits</string> <string name="lbl_bitrate">Tasa de bits</string>

View file

@ -61,14 +61,12 @@
<string name="info_app_desc">एंड्रॉयड के लिए एक सीधा साधा, विवेकशील गाने बजाने वाला ऐप।</string> <string name="info_app_desc">एंड्रॉयड के लिए एक सीधा साधा, विवेकशील गाने बजाने वाला ऐप।</string>
<string name="lbl_new_playlist">नई प्लेलिस्ट</string> <string name="lbl_new_playlist">नई प्लेलिस्ट</string>
<string name="lbl_play_next">अगला चलाएं</string> <string name="lbl_play_next">अगला चलाएं</string>
<string name="lbl_file_name">फ़ाइल का नाम</string>
<string name="set_lib_tabs">लायब्रेरी टैब्स</string> <string name="set_lib_tabs">लायब्रेरी टैब्स</string>
<string name="set_play_song_from_album">एल्बम से चलाएं</string> <string name="set_play_song_from_album">एल्बम से चलाएं</string>
<string name="set_content">सामग्री</string> <string name="set_content">सामग्री</string>
<string name="fmt_selected">%d चयनित</string> <string name="fmt_selected">%d चयनित</string>
<string name="lbl_format">प्रारूप</string> <string name="lbl_format">प्रारूप</string>
<string name="lbl_playlist_add">प्लेलिस्ट में जोड़ें</string> <string name="lbl_playlist_add">प्लेलिस्ट में जोड़ें</string>
<string name="lbl_relative_path">मुख्य पथ</string>
<string name="lbl_bitrate">बिट-रेट</string> <string name="lbl_bitrate">बिट-रेट</string>
<string name="lbl_cancel">रद्द करें</string> <string name="lbl_cancel">रद्द करें</string>
<string name="lbl_save">सहेजें</string> <string name="lbl_save">सहेजें</string>

View file

@ -40,8 +40,6 @@
<string name="lbl_queue">Popis pjesama</string> <string name="lbl_queue">Popis pjesama</string>
<string name="lbl_play_next">Reproduciraj sljedeću</string> <string name="lbl_play_next">Reproduciraj sljedeću</string>
<string name="lbl_props">Svojstva pjesme</string> <string name="lbl_props">Svojstva pjesme</string>
<string name="lbl_file_name">Naziv datoteke</string>
<string name="lbl_relative_path">Glavni direktorij</string>
<string name="lbl_format">Format</string> <string name="lbl_format">Format</string>
<string name="lbl_size">Veličina</string> <string name="lbl_size">Veličina</string>
<string name="lbl_bitrate">Brzina prijenosa</string> <string name="lbl_bitrate">Brzina prijenosa</string>

View file

@ -83,7 +83,6 @@
<string name="desc_shuffle">Keverés be/ki kapcsolása</string> <string name="desc_shuffle">Keverés be/ki kapcsolása</string>
<string name="desc_album_cover">%s album borítója</string> <string name="desc_album_cover">%s album borítója</string>
<string name="set_replay_gain">Visszajátszás</string> <string name="set_replay_gain">Visszajátszás</string>
<string name="lbl_relative_path">Szülő útvonal</string>
<string name="desc_music_dir_delete">Mappa eltávolítása</string> <string name="desc_music_dir_delete">Mappa eltávolítása</string>
<string name="lbl_playlist_add">Lejátszólistához ad</string> <string name="lbl_playlist_add">Lejátszólistához ad</string>
<string name="lbl_format">Formátum</string> <string name="lbl_format">Formátum</string>
@ -160,7 +159,6 @@
<string name="set_replay_gain_mode_dynamic">Inkább album, ha egyet játszik</string> <string name="set_replay_gain_mode_dynamic">Inkább album, ha egyet játszik</string>
<string name="set_reindex">Zene frissítése</string> <string name="set_reindex">Zene frissítése</string>
<string name="lbl_cancel">Mégse</string> <string name="lbl_cancel">Mégse</string>
<string name="lbl_file_name">Fájl név</string>
<string name="set_bar_action">Egyéni lejátszási sáv művelet</string> <string name="set_bar_action">Egyéni lejátszási sáv művelet</string>
<string name="set_headset_autoplay_desc">A lejátszás mindig akkor indul el, ha a fejhallgató csatlakoztatva van (nem minden eszközön működik)</string> <string name="set_headset_autoplay_desc">A lejátszás mindig akkor indul el, ha a fejhallgató csatlakoztatva van (nem minden eszközön működik)</string>
<string name="set_observing">Automatikus újratöltés</string> <string name="set_observing">Automatikus újratöltés</string>

View file

@ -52,7 +52,6 @@
<item quantity="other">%d Album</item> <item quantity="other">%d Album</item>
</plurals> </plurals>
<string name="lbl_props">Properti lagu</string> <string name="lbl_props">Properti lagu</string>
<string name="lbl_file_name">Nama berkas</string>
<string name="lbl_bitrate">Laju bit</string> <string name="lbl_bitrate">Laju bit</string>
<string name="lbl_ok">OK</string> <string name="lbl_ok">OK</string>
<string name="lbl_cancel">Batal</string> <string name="lbl_cancel">Batal</string>
@ -65,7 +64,6 @@
<string name="set_headset_autoplay">Putar otomatis headset</string> <string name="set_headset_autoplay">Putar otomatis headset</string>
<string name="set_headset_autoplay_desc">Selalu mulai memutar ketika headset tersambung (mungkin tidak berfungsi pada semua perangkat)</string> <string name="set_headset_autoplay_desc">Selalu mulai memutar ketika headset tersambung (mungkin tidak berfungsi pada semua perangkat)</string>
<string name="set_replay_gain_mode">Strategi ReplayGain</string> <string name="set_replay_gain_mode">Strategi ReplayGain</string>
<string name="lbl_relative_path">Path induk</string>
<string name="lbl_size">Ukuran</string> <string name="lbl_size">Ukuran</string>
<string name="lbl_sample_rate">Tingkat sampel</string> <string name="lbl_sample_rate">Tingkat sampel</string>
<string name="set_lib_tabs">Tab Pustaka</string> <string name="set_lib_tabs">Tab Pustaka</string>

View file

@ -181,8 +181,6 @@
<string name="lbl_sample_rate">Frequenza di campionamento</string> <string name="lbl_sample_rate">Frequenza di campionamento</string>
<string name="lbl_song_detail">Vedi proprietà</string> <string name="lbl_song_detail">Vedi proprietà</string>
<string name="lbl_props">Proprietà brano</string> <string name="lbl_props">Proprietà brano</string>
<string name="lbl_file_name">Nome file</string>
<string name="lbl_relative_path">Directory superiore</string>
<string name="lbl_format">Formato</string> <string name="lbl_format">Formato</string>
<string name="lbl_size">Dimensione</string> <string name="lbl_size">Dimensione</string>
<string name="lbl_bitrate">Bitrate</string> <string name="lbl_bitrate">Bitrate</string>

View file

@ -131,7 +131,6 @@
<string name="lbl_track">רצועה</string> <string name="lbl_track">רצועה</string>
<string name="lbl_queue">תור</string> <string name="lbl_queue">תור</string>
<string name="lbl_artist_details">מעבר לאומן</string> <string name="lbl_artist_details">מעבר לאומן</string>
<string name="lbl_file_name">שם קובץ</string>
<string name="lbl_shuffle_shortcut_short">ערבוב</string> <string name="lbl_shuffle_shortcut_short">ערבוב</string>
<string name="lbl_state_restored">המצב שוחזר</string> <string name="lbl_state_restored">המצב שוחזר</string>
<string name="lbl_about">אודות</string> <string name="lbl_about">אודות</string>
@ -242,7 +241,6 @@
<string name="desc_playlist_image">תמונת רשימת השמעה עבור %s</string> <string name="desc_playlist_image">תמונת רשימת השמעה עבור %s</string>
<string name="clr_red">אדום</string> <string name="clr_red">אדום</string>
<string name="clr_green">ירוק</string> <string name="clr_green">ירוק</string>
<string name="lbl_relative_path">נתיב ראשי</string>
<string name="err_did_not_restore">לא ניתן לשחזר את המצב</string> <string name="err_did_not_restore">לא ניתן לשחזר את המצב</string>
<string name="desc_track_number">רצועה %d</string> <string name="desc_track_number">רצועה %d</string>
<string name="desc_new_playlist">יצירת רשימת השמעה חדשה</string> <string name="desc_new_playlist">יצירת רשימת השמעה חדשה</string>

View file

@ -57,7 +57,6 @@
<string name="set_play_song_none">表示されたアイテムから再生</string> <string name="set_play_song_none">表示されたアイテムから再生</string>
<string name="desc_exit">再生停止</string> <string name="desc_exit">再生停止</string>
<string name="clr_red"></string> <string name="clr_red"></string>
<string name="lbl_file_name">ファイル名</string>
<string name="lbl_date_added">追加した日付け</string> <string name="lbl_date_added">追加した日付け</string>
<string name="lbl_sample_rate">サンプルレート</string> <string name="lbl_sample_rate">サンプルレート</string>
<string name="lbl_sort_dsc">降順</string> <string name="lbl_sort_dsc">降順</string>
@ -245,7 +244,6 @@
<string name="set_cover_mode_media_store">初期 (高速読み込み)</string> <string name="set_cover_mode_media_store">初期 (高速読み込み)</string>
<string name="set_replay_gain_mode_dynamic">再生中の場合はアルバムを優先</string> <string name="set_replay_gain_mode_dynamic">再生中の場合はアルバムを優先</string>
<string name="set_separators">複数値セパレータ</string> <string name="set_separators">複数値セパレータ</string>
<string name="lbl_relative_path">親パス</string>
<string name="set_observing_desc">変更されるたびに音楽ライブラリをリロードします (永続的な通知が必要です)</string> <string name="set_observing_desc">変更されるたびに音楽ライブラリをリロードします (永続的な通知が必要です)</string>
<string name="set_audio_desc">サウンドと再生の動作を構成する</string> <string name="set_audio_desc">サウンドと再生の動作を構成する</string>
<string name="set_rewind_prev">戻る前に巻き戻す</string> <string name="set_rewind_prev">戻る前に巻き戻す</string>

View file

@ -163,7 +163,6 @@
<string name="set_dirs_mode_include_desc"><b>추가한 폴더에서만</b> 음악을 불러옵니다.</string> <string name="set_dirs_mode_include_desc"><b>추가한 폴더에서만</b> 음악을 불러옵니다.</string>
<string name="lbl_props">곡 속성</string> <string name="lbl_props">곡 속성</string>
<string name="lbl_song_detail">속성 보기</string> <string name="lbl_song_detail">속성 보기</string>
<string name="lbl_file_name">파일 이름</string>
<string name="lbl_sample_rate">샘플 속도</string> <string name="lbl_sample_rate">샘플 속도</string>
<string name="lbl_bitrate">전송 속도</string> <string name="lbl_bitrate">전송 속도</string>
<string name="lbl_size">크기</string> <string name="lbl_size">크기</string>
@ -195,7 +194,6 @@
<string name="set_separators_and">앰퍼샌드 (&amp;)</string> <string name="set_separators_and">앰퍼샌드 (&amp;)</string>
<string name="cdc_mp3">MPEG-1 오디오</string> <string name="cdc_mp3">MPEG-1 오디오</string>
<string name="lbl_date_added">추가한 날짜</string> <string name="lbl_date_added">추가한 날짜</string>
<string name="lbl_relative_path">상위 경로</string>
<string name="set_bar_action">맞춤형 재생 동작 버튼</string> <string name="set_bar_action">맞춤형 재생 동작 버튼</string>
<string name="set_action_mode_repeat">반복 방식</string> <string name="set_action_mode_repeat">반복 방식</string>
<string name="desc_queue_bar">대기열 열기</string> <string name="desc_queue_bar">대기열 열기</string>

View file

@ -36,7 +36,6 @@
<string name="lbl_shuffle">Maišyti</string> <string name="lbl_shuffle">Maišyti</string>
<string name="lng_queue_added">Pridėtas į eilę</string> <string name="lng_queue_added">Pridėtas į eilę</string>
<string name="lbl_props">Dainų ypatybės</string> <string name="lbl_props">Dainų ypatybės</string>
<string name="lbl_file_name">Failo pavadinimas</string>
<string name="lbl_save">Išsaugoti</string> <string name="lbl_save">Išsaugoti</string>
<string name="lbl_about">Apie</string> <string name="lbl_about">Apie</string>
<string name="lbl_add">Pridėti</string> <string name="lbl_add">Pridėti</string>
@ -214,7 +213,6 @@
<string name="lbl_mix">DJ miksas</string> <string name="lbl_mix">DJ miksas</string>
<string name="lbl_compilation_live">Gyvai kompiliacija</string> <string name="lbl_compilation_live">Gyvai kompiliacija</string>
<string name="lbl_compilation_remix">Remikso kompiliacija</string> <string name="lbl_compilation_remix">Remikso kompiliacija</string>
<string name="lbl_relative_path">Pirminis kelias</string>
<string name="set_wipe_desc">Išvalyti anksčiau išsaugotą grojimo būseną (jei yra)</string> <string name="set_wipe_desc">Išvalyti anksčiau išsaugotą grojimo būseną (jei yra)</string>
<string name="set_separators">Daugiareikšmiai separatoriai</string> <string name="set_separators">Daugiareikšmiai separatoriai</string>
<string name="set_separators_slash">Pasvirasis brūkšnys (/)</string> <string name="set_separators_slash">Pasvirasis brūkšnys (/)</string>

View file

@ -25,7 +25,6 @@
<string name="lbl_size">വലിപ്പം</string> <string name="lbl_size">വലിപ്പം</string>
<string name="lbl_add">ചേർക്കുക</string> <string name="lbl_add">ചേർക്കുക</string>
<string name="lbl_ok">ശരി</string> <string name="lbl_ok">ശരി</string>
<string name="lbl_relative_path">ഉത്ഭവ പാത</string>
<string name="lbl_cancel">റദ്ദാക്കുക</string> <string name="lbl_cancel">റദ്ദാക്കുക</string>
<string name="set_theme_day">വെളിച്ചം</string> <string name="set_theme_day">വെളിച്ചം</string>
<string name="lbl_about">കുറിച്ച്</string> <string name="lbl_about">കുറിച്ച്</string>

View file

@ -192,8 +192,6 @@
<string name="lbl_format">Format</string> <string name="lbl_format">Format</string>
<string name="lbl_song_detail">Vis egenskaper</string> <string name="lbl_song_detail">Vis egenskaper</string>
<string name="lbl_props">Spor-egenskaper</string> <string name="lbl_props">Spor-egenskaper</string>
<string name="lbl_file_name">Filnavn</string>
<string name="lbl_relative_path">Overnevnt sti</string>
<string name="set_repeat_pause">Pause ved gjentagelse</string> <string name="set_repeat_pause">Pause ved gjentagelse</string>
<string name="clr_red">Rød</string> <string name="clr_red">Rød</string>
<plurals name="fmt_album_count"> <plurals name="fmt_album_count">

View file

@ -121,9 +121,7 @@
<string name="lbl_cancel">Annuleren</string> <string name="lbl_cancel">Annuleren</string>
<string name="set_lib_tabs">Bibliotheek tabbladen</string> <string name="set_lib_tabs">Bibliotheek tabbladen</string>
<string name="lbl_date">Jaar</string> <string name="lbl_date">Jaar</string>
<string name="lbl_relative_path">Ouderpad</string>
<string name="lbl_props">Lied eigenschappen</string> <string name="lbl_props">Lied eigenschappen</string>
<string name="lbl_file_name">Bestandsnaam</string>
<string name="set_replay_gain_mode_dynamic">Voorkeur album als er een speelt</string> <string name="set_replay_gain_mode_dynamic">Voorkeur album als er een speelt</string>
<string name="set_replay_gain_mode_track">Voorkeur titel</string> <string name="set_replay_gain_mode_track">Voorkeur titel</string>
<string name="set_replay_gain_mode_album">Voorkeur album</string> <string name="set_replay_gain_mode_album">Voorkeur album</string>

View file

@ -55,7 +55,6 @@
<string name="lbl_album_details">ਐਲਬਮ \'ਤੇ ਜਾਓ</string> <string name="lbl_album_details">ਐਲਬਮ \'ਤੇ ਜਾਓ</string>
<string name="lbl_song_detail">ਵਿਸ਼ੇਸ਼ਤਾਵਾਂ ਵੇਖੋ</string> <string name="lbl_song_detail">ਵਿਸ਼ੇਸ਼ਤਾਵਾਂ ਵੇਖੋ</string>
<string name="lbl_props">ਗੀਤ ਦੀਆਂ ਵਿਸ਼ੇਸ਼ਤਾਵਾਂ</string> <string name="lbl_props">ਗੀਤ ਦੀਆਂ ਵਿਸ਼ੇਸ਼ਤਾਵਾਂ</string>
<string name="lbl_relative_path">ਪੇਰੈਂਟ ਮਾਰਗ</string>
<string name="lbl_format">ਫਾਰਮੈਟ</string> <string name="lbl_format">ਫਾਰਮੈਟ</string>
<string name="lbl_size">ਆਕਾਰ</string> <string name="lbl_size">ਆਕਾਰ</string>
<string name="lbl_shuffle_shortcut_short">ਸ਼ਫਲ</string> <string name="lbl_shuffle_shortcut_short">ਸ਼ਫਲ</string>
@ -76,7 +75,6 @@
<string name="lbl_song_count">ਗੀਤ ਦੀ ਗਿਣਤੀ</string> <string name="lbl_song_count">ਗੀਤ ਦੀ ਗਿਣਤੀ</string>
<string name="lbl_sort_dsc">ਘਟਦੇ ਹੋਏ</string> <string name="lbl_sort_dsc">ਘਟਦੇ ਹੋਏ</string>
<string name="lbl_artist_details">ਕਲਾਕਾਰ \'ਤੇ ਜਾਓ</string> <string name="lbl_artist_details">ਕਲਾਕਾਰ \'ਤੇ ਜਾਓ</string>
<string name="lbl_file_name">ਫਾਈਲ ਦਾ ਨਾਮ</string>
<string name="lbl_bitrate">ਬਿੱਟ ਰੇਟ</string> <string name="lbl_bitrate">ਬਿੱਟ ਰੇਟ</string>
<string name="lbl_sample_rate">ਸੈਂਪਲ ਰੇਟ</string> <string name="lbl_sample_rate">ਸੈਂਪਲ ਰੇਟ</string>
<string name="lbl_add">ਸ਼ਾਮਿਲ ਕਰੋ</string> <string name="lbl_add">ਸ਼ਾਮਿਲ ਕਰੋ</string>

View file

@ -90,8 +90,6 @@
<string name="set_headset_autoplay">Autoodtwarzanie w słuchawkach</string> <string name="set_headset_autoplay">Autoodtwarzanie w słuchawkach</string>
<string name="clr_teal">Morski</string> <string name="clr_teal">Morski</string>
<string name="fmt_db_pos">+%.1f dB</string> <string name="fmt_db_pos">+%.1f dB</string>
<string name="lbl_file_name">Nazwa pliku</string>
<string name="lbl_relative_path">Ścieżka katalogu</string>
<string name="lbl_format">Format</string> <string name="lbl_format">Format</string>
<string name="clr_cyan">Niebieskozielony</string> <string name="clr_cyan">Niebieskozielony</string>
<string name="fmt_disc_no">Płyta %d</string> <string name="fmt_disc_no">Płyta %d</string>

View file

@ -106,7 +106,6 @@
<string name="lbl_artist">Artista</string> <string name="lbl_artist">Artista</string>
<string name="lbl_album">Álbum</string> <string name="lbl_album">Álbum</string>
<string name="lbl_props">Propriedades da música</string> <string name="lbl_props">Propriedades da música</string>
<string name="lbl_file_name">Nome do arquivo</string>
<string name="lbl_format">Formato</string> <string name="lbl_format">Formato</string>
<string name="lbl_size">Tamanho</string> <string name="lbl_size">Tamanho</string>
<string name="lbl_bitrate">Taxa de bits</string> <string name="lbl_bitrate">Taxa de bits</string>
@ -158,7 +157,6 @@
<string name="set_pre_amp_without">Ajuste em faixas sem metadados</string> <string name="set_pre_amp_without">Ajuste em faixas sem metadados</string>
<string name="set_headset_autoplay">Reprodução automática em fones de ouvido</string> <string name="set_headset_autoplay">Reprodução automática em fones de ouvido</string>
<string name="set_pre_amp">Pré-amplificação da normalização de volume</string> <string name="set_pre_amp">Pré-amplificação da normalização de volume</string>
<string name="lbl_relative_path">Caminho principal</string>
<string name="lbl_ok">OK</string> <string name="lbl_ok">OK</string>
<string name="set_display">Exibição</string> <string name="set_display">Exibição</string>
<string name="set_round_mode_desc">Ativa cantos arredondados em elementos adicionais da interface do usuário</string> <string name="set_round_mode_desc">Ativa cantos arredondados em elementos adicionais da interface do usuário</string>

View file

@ -115,7 +115,6 @@
<string name="lbl_sample_rate">Taxa de amostragem</string> <string name="lbl_sample_rate">Taxa de amostragem</string>
<string name="lbl_save">Salvar</string> <string name="lbl_save">Salvar</string>
<string name="set_separators">Separadores multi-valor</string> <string name="set_separators">Separadores multi-valor</string>
<string name="lbl_file_name">Nome do ficheiro</string>
<string name="lbl_size">Tamanho</string> <string name="lbl_size">Tamanho</string>
<string name="lbl_song_detail">Propriedades</string> <string name="lbl_song_detail">Propriedades</string>
<string name="lbl_props">Propriedades da música</string> <string name="lbl_props">Propriedades da música</string>
@ -217,7 +216,6 @@
<string name="fmt_indexing">A carregar a sua biblioteca de músicas… (%1$d/%2$d)</string> <string name="fmt_indexing">A carregar a sua biblioteca de músicas… (%1$d/%2$d)</string>
<string name="set_rewind_prev">Retroceder antes de voltar</string> <string name="set_rewind_prev">Retroceder antes de voltar</string>
<string name="desc_exit">Parar reprodução</string> <string name="desc_exit">Parar reprodução</string>
<string name="lbl_relative_path">Caminho principal</string>
<string name="set_round_mode_desc">Ativar cantos arredondados em elementos adicionais da interface do utilizador (requer que as capas dos álbuns sejam arredondadas)</string> <string name="set_round_mode_desc">Ativar cantos arredondados em elementos adicionais da interface do utilizador (requer que as capas dos álbuns sejam arredondadas)</string>
<string name="fmt_selected">%d Selecionadas</string> <string name="fmt_selected">%d Selecionadas</string>
<string name="lbl_mixes">Misturas DJ</string> <string name="lbl_mixes">Misturas DJ</string>

View file

@ -103,10 +103,8 @@
<string name="lbl_equalizer">Egalizator</string> <string name="lbl_equalizer">Egalizator</string>
<string name="lbl_bitrate">Bit rate</string> <string name="lbl_bitrate">Bit rate</string>
<string name="lbl_date_added">Data adăugării</string> <string name="lbl_date_added">Data adăugării</string>
<string name="lbl_relative_path">Calea principală</string>
<string name="lbl_format">Format</string> <string name="lbl_format">Format</string>
<string name="lbl_props">Proprietățile cântecului</string> <string name="lbl_props">Proprietățile cântecului</string>
<string name="lbl_file_name">Numele fișierului</string>
<string name="lbl_shuffle_shortcut_short">Amestecare</string> <string name="lbl_shuffle_shortcut_short">Amestecare</string>
<string name="lbl_add">Adaugă</string> <string name="lbl_add">Adaugă</string>
<string name="lbl_sample_rate">Frecvența de eșantionare</string> <string name="lbl_sample_rate">Frecvența de eșantionare</string>

View file

@ -157,7 +157,6 @@
<string name="set_pre_amp_warning">Внимание: Изменение предусиления на большое положительное значение может привести к появлению искажений на некоторых звуковых дорожках.</string> <string name="set_pre_amp_warning">Внимание: Изменение предусиления на большое положительное значение может привести к появлению искажений на некоторых звуковых дорожках.</string>
<string name="lbl_song_detail">Сведения</string> <string name="lbl_song_detail">Сведения</string>
<string name="lbl_props">Свойства трека</string> <string name="lbl_props">Свойства трека</string>
<string name="lbl_relative_path">Путь</string>
<string name="lbl_format">Формат</string> <string name="lbl_format">Формат</string>
<string name="lbl_size">Размер</string> <string name="lbl_size">Размер</string>
<string name="lbl_sample_rate">Частота дискретизации</string> <string name="lbl_sample_rate">Частота дискретизации</string>
@ -165,7 +164,6 @@
<string name="lbl_library_counts">Статистика библиотеки</string> <string name="lbl_library_counts">Статистика библиотеки</string>
<string name="set_restore_state">Восстановить состояние воспроизведения</string> <string name="set_restore_state">Восстановить состояние воспроизведения</string>
<string name="lbl_duration">Продолжительность</string> <string name="lbl_duration">Продолжительность</string>
<string name="lbl_file_name">Имя файла</string>
<string name="lbl_ep">Мини-альбом</string> <string name="lbl_ep">Мини-альбом</string>
<string name="lbl_eps">Мини-альбомы</string> <string name="lbl_eps">Мини-альбомы</string>
<string name="lbl_single">Сингл</string> <string name="lbl_single">Сингл</string>

View file

@ -59,7 +59,6 @@
<string name="set_save_desc">Shrani trenutno stanje predvajanja zdaj</string> <string name="set_save_desc">Shrani trenutno stanje predvajanja zdaj</string>
<string name="desc_skip_prev">Preskoči na zadnjo pesem</string> <string name="desc_skip_prev">Preskoči na zadnjo pesem</string>
<string name="set_observing_desc">Ponovno naloži glasbeno knjižnico vsakič, ko se zazna sprememba (zahteva vztrajno obvestilo)</string> <string name="set_observing_desc">Ponovno naloži glasbeno knjižnico vsakič, ko se zazna sprememba (zahteva vztrajno obvestilo)</string>
<string name="lbl_relative_path">Pot do datoteke</string>
<plurals name="fmt_song_count"> <plurals name="fmt_song_count">
<item quantity="one">%d pesem</item> <item quantity="one">%d pesem</item>
<item quantity="two">%d pesmi</item> <item quantity="two">%d pesmi</item>
@ -79,7 +78,6 @@
<string name="lbl_mixtapes">Mešanice</string> <string name="lbl_mixtapes">Mešanice</string>
<string name="lbl_artist">Izvajalec</string> <string name="lbl_artist">Izvajalec</string>
<string name="set_intelligent_sorting_desc">Pravilno razvrsti imena, ki se začnejo z številkami ali besedami, kot so \'the\' (najbolje deluje z angleško glasbo)</string> <string name="set_intelligent_sorting_desc">Pravilno razvrsti imena, ki se začnejo z številkami ali besedami, kot so \'the\' (najbolje deluje z angleško glasbo)</string>
<string name="lbl_file_name">Ime datoteke</string>
<string name="clr_teal">Zelenkasto modra</string> <string name="clr_teal">Zelenkasto modra</string>
<string name="set_state">Vztrajnost</string> <string name="set_state">Vztrajnost</string>
<string name="desc_shuffle_all">Premešaj vse pesmi</string> <string name="desc_shuffle_all">Premešaj vse pesmi</string>

View file

@ -52,7 +52,6 @@
<string name="lbl_song_detail">Visa egenskaper</string> <string name="lbl_song_detail">Visa egenskaper</string>
<string name="lbl_share">Dela</string> <string name="lbl_share">Dela</string>
<string name="lbl_props">Egenskaper för låt</string> <string name="lbl_props">Egenskaper för låt</string>
<string name="lbl_relative_path">Överordnad mapp</string>
<string name="lbl_format">Format</string> <string name="lbl_format">Format</string>
<string name="lbl_size">Storlek</string> <string name="lbl_size">Storlek</string>
<string name="lbl_sample_rate">Samplingsfrekvens</string> <string name="lbl_sample_rate">Samplingsfrekvens</string>
@ -99,7 +98,6 @@
<string name="lbl_disc">Disk</string> <string name="lbl_disc">Disk</string>
<string name="lbl_sort">Sortera</string> <string name="lbl_sort">Sortera</string>
<string name="lbl_queue_add">Lägg till kö</string> <string name="lbl_queue_add">Lägg till kö</string>
<string name="lbl_file_name">Filnamn</string>
<string name="lbl_add">Lägg till</string> <string name="lbl_add">Lägg till</string>
<string name="lbl_state_wiped">Tillstånd tog bort</string> <string name="lbl_state_wiped">Tillstånd tog bort</string>
<string name="lbl_bitrate">Bithastighet</string> <string name="lbl_bitrate">Bithastighet</string>

View file

@ -52,10 +52,8 @@
<item quantity="one">%d albüm</item> <item quantity="one">%d albüm</item>
<item quantity="other">%d albümler</item> <item quantity="other">%d albümler</item>
</plurals> </plurals>
<string name="lbl_file_name">Dosya adı</string>
<string name="lbl_song_detail">Özellikleri görüntüle</string> <string name="lbl_song_detail">Özellikleri görüntüle</string>
<string name="lbl_props">Şarkı özellikleri</string> <string name="lbl_props">Şarkı özellikleri</string>
<string name="lbl_relative_path">Ana yol</string>
<string name="lbl_format">Biçim</string> <string name="lbl_format">Biçim</string>
<string name="lbl_shuffle_shortcut_short">Karıştır</string> <string name="lbl_shuffle_shortcut_short">Karıştır</string>
<string name="lbl_shuffle_shortcut_long">Hepsini karıştır</string> <string name="lbl_shuffle_shortcut_long">Hepsini karıştır</string>

View file

@ -54,7 +54,6 @@
<item quantity="many">%d альбомів</item> <item quantity="many">%d альбомів</item>
<item quantity="other">%d альбомів</item> <item quantity="other">%d альбомів</item>
</plurals> </plurals>
<string name="lbl_file_name">Ім\'я файлу</string>
<string name="lbl_format">Формат</string> <string name="lbl_format">Формат</string>
<string name="lbl_ok">Добре</string> <string name="lbl_ok">Добре</string>
<string name="lbl_cancel">Скасувати</string> <string name="lbl_cancel">Скасувати</string>
@ -79,7 +78,6 @@
<string name="lbl_compilations">Збірки</string> <string name="lbl_compilations">Збірки</string>
<string name="lbl_compilation">Збірка</string> <string name="lbl_compilation">Збірка</string>
<string name="lbl_album_live">Концертний альбом</string> <string name="lbl_album_live">Концертний альбом</string>
<string name="lbl_relative_path">Шлях до каталогу</string>
<string name="set_display">Екран</string> <string name="set_display">Екран</string>
<string name="lbl_date">Рік</string> <string name="lbl_date">Рік</string>
<string name="set_cover_mode">Обкладинки альбомів</string> <string name="set_cover_mode">Обкладинки альбомів</string>

View file

@ -176,8 +176,6 @@
<string name="lbl_track">音轨</string> <string name="lbl_track">音轨</string>
<string name="lbl_song_detail">查看属性</string> <string name="lbl_song_detail">查看属性</string>
<string name="lbl_props">曲目属性</string> <string name="lbl_props">曲目属性</string>
<string name="lbl_file_name">文件名</string>
<string name="lbl_relative_path">上级目录</string>
<string name="lbl_format">格式</string> <string name="lbl_format">格式</string>
<string name="lbl_size">大小</string> <string name="lbl_size">大小</string>
<string name="lbl_bitrate">比特率</string> <string name="lbl_bitrate">比特率</string>

View file

@ -135,8 +135,7 @@
<string name="lbl_share">Share</string> <string name="lbl_share">Share</string>
<string name="lbl_props">Song properties</string> <string name="lbl_props">Song properties</string>
<string name="lbl_file_name">File name</string> <string name="lbl_path">Path</string>
<string name="lbl_relative_path">Parent path</string>
<!-- As in audio format --> <!-- As in audio format -->
<string name="lbl_format">Format</string> <string name="lbl_format">Format</string>
<!-- As in file size --> <!-- As in file size -->