2
.gitignore
vendored
|
@ -14,3 +14,5 @@ captures/
|
|||
*.iml
|
||||
.cxx
|
||||
.kotlin
|
||||
.aider*
|
||||
.env
|
||||
|
|
18
CHANGELOG.md
|
@ -1,5 +1,23 @@
|
|||
# Changelog
|
||||
|
||||
## 4.0.2
|
||||
|
||||
#### What's New
|
||||
- Added back in support for cover art from cover.png/cover.jpg
|
||||
- Added "As is" cover art setting
|
||||
- Option to include hidden files or not (off by default)
|
||||
|
||||
#### What's Improved
|
||||
- Reduced elevation contrast in black theme
|
||||
|
||||
#### What's Fixed
|
||||
- Fixed incorrect extension stripping on some files
|
||||
- Fixed various errors in new branding
|
||||
- Fixed MTE segfault from improper string handling
|
||||
|
||||
#### What's Changed
|
||||
- Hidden files no longer loaded by default
|
||||
|
||||
## 4.0.1
|
||||
|
||||
#### What's Fixed
|
||||
|
|
|
@ -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/v4.0.1">
|
||||
<img alt="Latest Version" src="https://img.shields.io/static/v1?label=tag&message=v4.0.1&color=64B5F6&style=flat">
|
||||
<a href="https://github.com/oxygencobalt/Auxio/releases/tag/v4.0.2">
|
||||
<img alt="Latest Version" src="https://img.shields.io/static/v1?label=tag&message=v4.0.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">
|
||||
|
|
|
@ -18,8 +18,8 @@ android {
|
|||
|
||||
defaultConfig {
|
||||
applicationId namespace
|
||||
versionName "4.0.1"
|
||||
versionCode 60
|
||||
versionName "4.0.2"
|
||||
versionCode 61
|
||||
|
||||
minSdk min_sdk
|
||||
targetSdk target_sdk
|
||||
|
|
|
@ -141,4 +141,6 @@ object IntegerTable {
|
|||
const val PLAY_SONG_BY_ITSELF = 0xA124
|
||||
/** CoverMode.SaveSpace */
|
||||
const val COVER_MODE_SAVE_SPACE = 0xA125
|
||||
/** CoverMode.AsIs */
|
||||
const val COVER_MODE_AS_IS = 0xA126
|
||||
}
|
||||
|
|
|
@ -29,7 +29,8 @@ enum class CoverMode {
|
|||
OFF,
|
||||
SAVE_SPACE,
|
||||
BALANCED,
|
||||
HIGH_QUALITY;
|
||||
HIGH_QUALITY,
|
||||
AS_IS;
|
||||
|
||||
/**
|
||||
* The integer representation of this instance.
|
||||
|
@ -43,6 +44,7 @@ enum class CoverMode {
|
|||
SAVE_SPACE -> IntegerTable.COVER_MODE_SAVE_SPACE
|
||||
BALANCED -> IntegerTable.COVER_MODE_BALANCED
|
||||
HIGH_QUALITY -> IntegerTable.COVER_MODE_HIGH_QUALITY
|
||||
AS_IS -> IntegerTable.COVER_MODE_AS_IS
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -59,6 +61,7 @@ enum class CoverMode {
|
|||
IntegerTable.COVER_MODE_SAVE_SPACE -> SAVE_SPACE
|
||||
IntegerTable.COVER_MODE_BALANCED -> BALANCED
|
||||
IntegerTable.COVER_MODE_HIGH_QUALITY -> HIGH_QUALITY
|
||||
IntegerTable.COVER_MODE_AS_IS -> AS_IS
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,11 +27,10 @@ import android.net.Uri
|
|||
import android.os.ParcelFileDescriptor
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.oxycblt.auxio.BuildConfig
|
||||
import org.oxycblt.auxio.image.covers.SiloedCoverId
|
||||
import org.oxycblt.auxio.image.covers.SiloedCovers
|
||||
import org.oxycblt.musikr.cover.ObtainResult
|
||||
import org.oxycblt.auxio.image.covers.SettingCovers
|
||||
import org.oxycblt.musikr.cover.CoverResult
|
||||
|
||||
class CoverProvider : ContentProvider() {
|
||||
class CoverProvider() : ContentProvider() {
|
||||
override fun onCreate(): Boolean = true
|
||||
|
||||
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
|
||||
|
@ -39,12 +38,10 @@ class CoverProvider : ContentProvider() {
|
|||
return null
|
||||
}
|
||||
val id = uri.lastPathSegment ?: return null
|
||||
val coverId = SiloedCoverId.parse(id) ?: return null
|
||||
return runBlocking {
|
||||
val siloedCovers = SiloedCovers.from(requireNotNull(context), coverId.silo)
|
||||
when (val res = siloedCovers.obtain(id)) {
|
||||
is ObtainResult.Hit -> res.cover.fd()
|
||||
is ObtainResult.Miss -> null
|
||||
when (val result = SettingCovers.immutable(requireNotNull(context)).obtain(id)) {
|
||||
is CoverResult.Hit -> result.cover.fd()
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -409,7 +409,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
@Px val iconSize: Int?
|
||||
) : Drawable() {
|
||||
init {
|
||||
// Re-tint the drawable to use the analogous "on surfaceg" color for
|
||||
// Re-tint the drawable to use the analogous "on surface" color for
|
||||
// StyledImageView.
|
||||
DrawableCompat.setTintList(inner, context.getColorCompat(R.color.sel_on_cover_bg))
|
||||
}
|
||||
|
|
|
@ -21,17 +21,23 @@ package org.oxycblt.auxio.image.covers
|
|||
import java.util.UUID
|
||||
import org.oxycblt.musikr.cover.CoverParams
|
||||
|
||||
data class CoverSilo(val revision: UUID, val params: CoverParams) {
|
||||
override fun toString() = "${revision}.${params.resolution}.${params.quality}"
|
||||
data class CoverSilo(val revision: UUID, val params: CoverParams?) {
|
||||
override fun toString() =
|
||||
"${revision}${params?.let { ".${params.resolution}.${params.quality}" } ?: "" }"
|
||||
|
||||
companion object {
|
||||
fun parse(silo: String): CoverSilo? {
|
||||
val parts = silo.split('.')
|
||||
if (parts.size != 3) return null
|
||||
if (parts.size != 1 && parts.size != 3) {
|
||||
return null
|
||||
}
|
||||
val revision = parts[0].toUuidOrNull() ?: return null
|
||||
val resolution = parts[1].toIntOrNull() ?: return null
|
||||
val quality = parts[2].toIntOrNull() ?: return null
|
||||
return CoverSilo(revision, CoverParams.of(resolution, quality))
|
||||
if (parts.size > 1) {
|
||||
val resolution = parts[1].toIntOrNull() ?: return null
|
||||
val quality = parts[2].toIntOrNull() ?: return null
|
||||
return CoverSilo(revision, CoverParams.of(resolution, quality))
|
||||
}
|
||||
return CoverSilo(revision, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,13 +20,15 @@ package org.oxycblt.auxio.image.covers
|
|||
|
||||
import android.content.Context
|
||||
import org.oxycblt.musikr.cover.Cover
|
||||
import org.oxycblt.musikr.cover.CoverResult
|
||||
import org.oxycblt.musikr.cover.MutableCovers
|
||||
import org.oxycblt.musikr.cover.ObtainResult
|
||||
import org.oxycblt.musikr.fs.device.DeviceFile
|
||||
import org.oxycblt.musikr.metadata.Metadata
|
||||
|
||||
class NullCovers(private val context: Context) : MutableCovers {
|
||||
override suspend fun obtain(id: String) = ObtainResult.Hit(NullCover)
|
||||
class NullCovers(private val context: Context) : MutableCovers<NullCover> {
|
||||
override suspend fun obtain(id: String) = CoverResult.Hit(NullCover)
|
||||
|
||||
override suspend fun write(data: ByteArray): Cover = NullCover
|
||||
override suspend fun create(file: DeviceFile, metadata: Metadata) = CoverResult.Hit(NullCover)
|
||||
|
||||
override suspend fun cleanup(excluding: Collection<Cover>) {
|
||||
context.coversDir().listFiles()?.forEach { it.deleteRecursively() }
|
||||
|
|
|
@ -23,26 +23,39 @@ import java.util.UUID
|
|||
import javax.inject.Inject
|
||||
import org.oxycblt.auxio.image.CoverMode
|
||||
import org.oxycblt.auxio.image.ImageSettings
|
||||
import org.oxycblt.musikr.cover.Cover
|
||||
import org.oxycblt.musikr.cover.CoverIdentifier
|
||||
import org.oxycblt.musikr.cover.CoverParams
|
||||
import org.oxycblt.musikr.cover.Covers
|
||||
import org.oxycblt.musikr.cover.FileCover
|
||||
import org.oxycblt.musikr.cover.FolderCovers
|
||||
import org.oxycblt.musikr.cover.MutableCovers
|
||||
import org.oxycblt.musikr.cover.MutableFolderCovers
|
||||
|
||||
interface SettingCovers {
|
||||
suspend fun create(context: Context, revision: UUID): MutableCovers
|
||||
suspend fun mutate(context: Context, revision: UUID): MutableCovers<out Cover>
|
||||
|
||||
companion object {
|
||||
fun immutable(context: Context): Covers<FileCover> =
|
||||
Covers.chain(BaseSiloedCovers(context), FolderCovers(context))
|
||||
}
|
||||
}
|
||||
|
||||
class SettingCoversImpl
|
||||
@Inject
|
||||
constructor(private val imageSettings: ImageSettings, private val identifier: CoverIdentifier) :
|
||||
SettingCovers {
|
||||
override suspend fun create(context: Context, revision: UUID): MutableCovers =
|
||||
override suspend fun mutate(context: Context, revision: UUID): MutableCovers<out Cover> =
|
||||
when (imageSettings.coverMode) {
|
||||
CoverMode.OFF -> NullCovers(context)
|
||||
CoverMode.SAVE_SPACE -> siloedCovers(context, revision, CoverParams.of(500, 70))
|
||||
CoverMode.BALANCED -> siloedCovers(context, revision, CoverParams.of(750, 85))
|
||||
CoverMode.HIGH_QUALITY -> siloedCovers(context, revision, CoverParams.of(1000, 100))
|
||||
CoverMode.AS_IS -> siloedCovers(context, revision, null)
|
||||
}
|
||||
|
||||
private suspend fun siloedCovers(context: Context, revision: UUID, with: CoverParams) =
|
||||
MutableSiloedCovers.from(context, CoverSilo(revision, with), identifier)
|
||||
private suspend fun siloedCovers(context: Context, revision: UUID, with: CoverParams?) =
|
||||
MutableCovers.chain(
|
||||
MutableSiloedCovers.from(context, CoverSilo(revision, with), identifier),
|
||||
MutableFolderCovers(context))
|
||||
}
|
||||
|
|
|
@ -25,21 +25,36 @@ import kotlinx.coroutines.withContext
|
|||
import org.oxycblt.musikr.cover.Cover
|
||||
import org.oxycblt.musikr.cover.CoverFormat
|
||||
import org.oxycblt.musikr.cover.CoverIdentifier
|
||||
import org.oxycblt.musikr.cover.CoverResult
|
||||
import org.oxycblt.musikr.cover.Covers
|
||||
import org.oxycblt.musikr.cover.FileCover
|
||||
import org.oxycblt.musikr.cover.FileCovers
|
||||
import org.oxycblt.musikr.cover.MutableCovers
|
||||
import org.oxycblt.musikr.cover.MutableFileCovers
|
||||
import org.oxycblt.musikr.cover.ObtainResult
|
||||
import org.oxycblt.musikr.fs.app.AppFiles
|
||||
import org.oxycblt.musikr.fs.device.DeviceFile
|
||||
import org.oxycblt.musikr.metadata.Metadata
|
||||
|
||||
open class SiloedCovers(private val silo: CoverSilo, private val fileCovers: FileCovers) : Covers {
|
||||
override suspend fun obtain(id: String): ObtainResult<SiloedCover> {
|
||||
val coverId = SiloedCoverId.parse(id) ?: return ObtainResult.Miss()
|
||||
if (coverId.silo != silo) return ObtainResult.Miss()
|
||||
class BaseSiloedCovers(private val context: Context) : Covers<FileCover> {
|
||||
override suspend fun obtain(id: String): CoverResult<FileCover> {
|
||||
val siloedId = SiloedCoverId.parse(id) ?: return CoverResult.Miss()
|
||||
val core = SiloCore.from(context, siloedId.silo)
|
||||
val fileCovers = FileCovers(core.files, core.format)
|
||||
return when (val result = fileCovers.obtain(siloedId.id)) {
|
||||
is CoverResult.Hit -> CoverResult.Hit(SiloedCover(siloedId.silo, result.cover))
|
||||
is CoverResult.Miss -> CoverResult.Miss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open class SiloedCovers(private val silo: CoverSilo, private val fileCovers: FileCovers) :
|
||||
Covers<FileCover> {
|
||||
override suspend fun obtain(id: String): CoverResult<FileCover> {
|
||||
val coverId = SiloedCoverId.parse(id) ?: return CoverResult.Miss()
|
||||
if (silo != coverId.silo) return CoverResult.Miss()
|
||||
return when (val result = fileCovers.obtain(coverId.id)) {
|
||||
is ObtainResult.Hit -> ObtainResult.Hit(SiloedCover(silo, result.cover))
|
||||
is ObtainResult.Miss -> ObtainResult.Miss()
|
||||
is CoverResult.Hit -> CoverResult.Hit(SiloedCover(silo, result.cover))
|
||||
is CoverResult.Miss -> CoverResult.Miss()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -56,8 +71,12 @@ private constructor(
|
|||
private val rootDir: File,
|
||||
private val silo: CoverSilo,
|
||||
private val fileCovers: MutableFileCovers
|
||||
) : SiloedCovers(silo, fileCovers), MutableCovers {
|
||||
override suspend fun write(data: ByteArray) = SiloedCover(silo, fileCovers.write(data))
|
||||
) : SiloedCovers(silo, fileCovers), MutableCovers<FileCover> {
|
||||
override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult<FileCover> =
|
||||
when (val result = fileCovers.create(file, metadata)) {
|
||||
is CoverResult.Hit -> CoverResult.Hit(SiloedCover(silo, result.cover))
|
||||
is CoverResult.Miss -> CoverResult.Miss()
|
||||
}
|
||||
|
||||
override suspend fun cleanup(excluding: Collection<Cover>) {
|
||||
fileCovers.cleanup(excluding.filterIsInstance<SiloedCover>().map { it.innerCover })
|
||||
|
@ -111,7 +130,7 @@ private data class SiloCore(val rootDir: File, val files: AppFiles, val format:
|
|||
revisionDir = rootDir.resolve(silo.toString()).apply { mkdirs() }
|
||||
}
|
||||
val files = AppFiles.at(revisionDir)
|
||||
val format = CoverFormat.jpeg(silo.params)
|
||||
val format = silo.params?.let(CoverFormat::jpeg) ?: CoverFormat.asIs()
|
||||
return SiloCore(rootDir, files, format)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -384,13 +384,14 @@ constructor(
|
|||
Naming.simple()
|
||||
}
|
||||
val locations = musicSettings.musicLocations
|
||||
val ignoreHidden = !musicSettings.withHidden
|
||||
|
||||
val currentRevision = musicSettings.revision
|
||||
val newRevision = currentRevision?.takeIf { withCache } ?: UUID.randomUUID()
|
||||
val cache = if (withCache) storedCache.visible() else storedCache.invisible()
|
||||
val covers = settingCovers.create(context, newRevision)
|
||||
val covers = settingCovers.mutate(context, newRevision)
|
||||
val storage = Storage(cache, covers, storedPlaylists)
|
||||
val interpretation = Interpretation(nameFactory, separators)
|
||||
val interpretation = Interpretation(nameFactory, separators, ignoreHidden)
|
||||
|
||||
val result =
|
||||
Musikr.new(context, storage, interpretation).run(locations, ::emitIndexingProgress)
|
||||
|
|
|
@ -40,6 +40,8 @@ interface MusicSettings : Settings<MusicSettings.Listener> {
|
|||
var musicLocations: List<MusicLocation>
|
||||
/** Whether to exclude non-music audio files from the music library. */
|
||||
val excludeNonMusic: Boolean
|
||||
/** Whether to ignore hidden files and directories during music loading. */
|
||||
val withHidden: Boolean
|
||||
/** Whether to be actively watching for changes in the music library. */
|
||||
val shouldBeObserving: Boolean
|
||||
/** A [String] of characters representing the desired characters to denote multi-value tags. */
|
||||
|
@ -90,6 +92,9 @@ class MusicSettingsImpl @Inject constructor(@ApplicationContext private val cont
|
|||
override val excludeNonMusic: Boolean
|
||||
get() = sharedPreferences.getBoolean(getString(R.string.set_key_exclude_non_music), true)
|
||||
|
||||
override val withHidden: Boolean
|
||||
get() = sharedPreferences.getBoolean(getString(R.string.set_key_with_hidden), false)
|
||||
|
||||
override val shouldBeObserving: Boolean
|
||||
get() = sharedPreferences.getBoolean(getString(R.string.set_key_observing), false)
|
||||
|
||||
|
@ -116,7 +121,9 @@ class MusicSettingsImpl @Inject constructor(@ApplicationContext private val cont
|
|||
listener.onMusicLocationsChanged()
|
||||
}
|
||||
getString(R.string.set_key_separators),
|
||||
getString(R.string.set_key_auto_sort_names) -> {
|
||||
getString(R.string.set_key_auto_sort_names),
|
||||
getString(R.string.set_key_with_hidden),
|
||||
getString(R.string.set_key_exclude_non_music) -> {
|
||||
L.d("Dispatching indexing setting change for $key")
|
||||
listener.onIndexingSettingChanged()
|
||||
}
|
||||
|
|
|
@ -67,5 +67,14 @@ class MusicPreferenceFragment : BasePreferenceFragment(R.xml.preferences_music)
|
|||
true
|
||||
}
|
||||
}
|
||||
if (preference.key == getString(R.string.set_key_with_hidden)) {
|
||||
L.d("Configuring ignore hidden files setting")
|
||||
preference.onPreferenceChangeListener =
|
||||
Preference.OnPreferenceChangeListener { _, _ ->
|
||||
L.d("Ignore hidden files setting changed, reloading music")
|
||||
musicModel.refresh()
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,22 +65,22 @@ private val accentThemes =
|
|||
|
||||
private val accentBlackThemes =
|
||||
intArrayOf(
|
||||
R.style.Theme_Auxio_Black_Red,
|
||||
R.style.Theme_Auxio_Black_Pink,
|
||||
R.style.Theme_Auxio_Black_Purple,
|
||||
R.style.Theme_Auxio_Black_DeepPurple,
|
||||
R.style.Theme_Auxio_Black_Indigo,
|
||||
R.style.Theme_Auxio_Black_Blue,
|
||||
R.style.Theme_Auxio_Black_DeepBlue,
|
||||
R.style.Theme_Auxio_Black_Cyan,
|
||||
R.style.Theme_Auxio_Black_Teal,
|
||||
R.style.Theme_Auxio_Black_Green,
|
||||
R.style.Theme_Auxio_Black_DeepGreen,
|
||||
R.style.Theme_Auxio_Black_Lime,
|
||||
R.style.Theme_Auxio_Black_Yellow,
|
||||
R.style.Theme_Auxio_Black_Orange,
|
||||
R.style.Theme_Auxio_Black_Brown,
|
||||
R.style.Theme_Auxio_Black_Grey,
|
||||
R.style.Theme_Auxio_Red_Black,
|
||||
R.style.Theme_Auxio_Pink_Black,
|
||||
R.style.Theme_Auxio_Purple_Black,
|
||||
R.style.Theme_Auxio_DeepPurple_Black,
|
||||
R.style.Theme_Auxio_Indigo_Black,
|
||||
R.style.Theme_Auxio_Blue_Black,
|
||||
R.style.Theme_Auxio_DeepBlue_Black,
|
||||
R.style.Theme_Auxio_Cyan_Black,
|
||||
R.style.Theme_Auxio_Teal_Black,
|
||||
R.style.Theme_Auxio_Green_Black,
|
||||
R.style.Theme_Auxio_DeepGreen_Black,
|
||||
R.style.Theme_Auxio_Lime_Black,
|
||||
R.style.Theme_Auxio_Yellow_Black,
|
||||
R.style.Theme_Auxio_Orange_Black,
|
||||
R.style.Theme_Auxio_Brown_Black,
|
||||
R.style.Theme_Auxio_Grey_Black,
|
||||
R.style.Theme_Auxio_Black // Dynamic colors are on the base theme
|
||||
)
|
||||
|
||||
|
|
|
@ -266,4 +266,4 @@
|
|||
<string name="lng_empty_genres">الفئات الخاصة بك ستضهر هنا.</string>
|
||||
<string name="set_observing_desc">اعادة تحميل مكتبة الموسيقى عند حصول تغيير(يتطلب تنبيه ثابت)</string>
|
||||
<string name="set_separators_warning">تحذير: استخدام هذا الاعداد قد ينتج عنه ان يتم تفسير بعض العلامات بشكل خاطئ مثل ان تحتوي على قيم متعددة. يمكن ان يتم حل هذا بتقديم الفواصل الغير مرغوبةبالشارحة الخلفية(\\).</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
@ -230,7 +230,7 @@
|
|||
<string name="set_separators_comma">Koma (,)</string>
|
||||
<string name="set_separators_semicolon">Semikoolon (;)</string>
|
||||
<string name="set_separators_plus">Pluss (+)</string>
|
||||
<string name="set_separators_and">Ampersand (&)</string>
|
||||
<string name="set_separators_and">Ampersand (and-märk)</string>
|
||||
<string name="set_separators_desc">Seadista tähemärke, mis eraldavad siltides mitut väärtust</string>
|
||||
<string name="set_separators_slash">Kaldkriips (/)</string>
|
||||
<string name="set_separators_warning">Hoiatus: selle seadistuse kasutamisel ei pruugi mitu väärtust siltides olla alati korralikult tuvastatud; seda olukorda saad proovida lahendada täiendava prefiksi lisamisega kurakaldkriipsu näol (\\).</string>
|
||||
|
@ -324,4 +324,4 @@
|
|||
<string name="lng_empty_genres">Sinu žanrid saavad olema nähtavad siin.</string>
|
||||
<string name="lng_empty_albums">Sinu albumid saavad olema nähtavad siin.</string>
|
||||
<string name="lng_empty_playlists">Sinu esitusloendid saavad olema nähtavad siin.</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
@ -152,9 +152,9 @@
|
|||
<string name="set_pre_amp_without">Regolazione senza tag</string>
|
||||
<string name="lbl_shuffle_shortcut_short">Casuale</string>
|
||||
<string name="lbl_shuffle_shortcut_long">Tutto in casuale</string>
|
||||
<string name="set_pre_amp_with">Regolazione mediante tag</string>
|
||||
<string name="set_pre_amp_with">Regolazione in base ai tag</string>
|
||||
<string name="set_pre_amp_desc">Il pre-amp è applicato alla regolazione esistente durante la riproduzione</string>
|
||||
<string name="set_play_song_none">Riproduci dall\'elemento mostrato</string>
|
||||
<string name="set_play_song_none">Riproduci dall\'elemento corrente</string>
|
||||
<string name="set_locations_desc">Gestisci le cartelle da dove caricare la musica</string>
|
||||
<string name="set_locations">Cartelle musicali</string>
|
||||
<string name="cdc_mka">Matroska audio</string>
|
||||
|
@ -182,7 +182,7 @@
|
|||
<string name="set_observing_desc">Ricarica la tua libreria musicale se subisce cambiamenti (richiede notifica persistente)</string>
|
||||
<string name="lbl_indexing">Caricamento musica</string>
|
||||
<string name="lbl_observing">Monitoraggio libreria musicale</string>
|
||||
<string name="lbl_date_added">Data aggiunta</string>
|
||||
<string name="lbl_date_added">Data di aggiunta</string>
|
||||
<string name="set_observing">Ricaricamento automatico</string>
|
||||
<string name="lbl_eps">EP</string>
|
||||
<string name="lbl_ep">EP</string>
|
||||
|
@ -219,7 +219,7 @@
|
|||
<string name="set_separators">Separatori multi-valore</string>
|
||||
<string name="set_separators_desc">Configura i caratteri che identificano tag con valori multipli</string>
|
||||
<string name="set_separators_slash">Barra (/)</string>
|
||||
<string name="set_separators_warning">Attenzione: potrebbero verificarsi degli errori nell\'interpretazione di alcuni tag con valori multipli. Puoi risolvere aggiungendo come prefisso la barra rovesciata () ai separatori indesiderati.</string>
|
||||
<string name="set_separators_warning">Attenzione: potrebbero verificarsi degli errori nell\'interpretazione di alcuni tag con valori multipli. Puoi risolvere questo problema aggiungendo come prefisso una barra rovesciata (\\) ai separatori indesiderati.</string>
|
||||
<string name="set_separators_and">E commerciale (&)</string>
|
||||
<string name="lbl_compilation_live">Compilation live</string>
|
||||
<string name="lbl_compilation_remix">Compilation remix</string>
|
||||
|
@ -334,4 +334,4 @@
|
|||
<string name="cnt_mp4">MPEG-4 contenente %s</string>
|
||||
<string name="cdc_alac">Apple Lossless Audio Codec (ALAC)</string>
|
||||
<string name="cdc_unknown">Sconosciuto</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
@ -19,4 +19,91 @@
|
|||
<string name="lbl_ep_live">პირდაპირი EP</string>
|
||||
<string name="lbl_singles">სინგლები</string>
|
||||
<string name="lbl_single">სინგლი</string>
|
||||
<string name="lbl_album_live">Live ალბომები</string>
|
||||
<string name="lbl_album_remix">Remix ალბომები</string>
|
||||
<string name="lbl_artist">არტისტი</string>
|
||||
<string name="lbl_artists">არტისტები</string>
|
||||
<string name="lbl_genre">ჟანრი</string>
|
||||
<string name="lbl_genres">ჟანრები</string>
|
||||
<string name="lbl_playlist">დასაკრავი სია</string>
|
||||
<string name="lbl_playlists">დასაკრავი სიები</string>
|
||||
<string name="lbl_new_playlist">ახალი დასაკრავი სია</string>
|
||||
<string name="lbl_empty_playlist">ცარიელი დასაკრავი სია</string>
|
||||
<string name="lbl_import">იმპორტი</string>
|
||||
<string name="lbl_export">ექსპორტი</string>
|
||||
<string name="lbl_rename">სახელის გადარქმევა</string>
|
||||
<string name="lbl_imported_playlist">დასაკრავი სია იმპორტირებულია</string>
|
||||
<string name="lbl_import_playlist">დასაკრავი სიის იმპორტი</string>
|
||||
<string name="lbl_export_playlist">დასაკრავი სიის ექსპორტი</string>
|
||||
<string name="lbl_delete">წაშლა</string>
|
||||
<string name="lbl_confirm_delete_playlist">დასაკრავი სიის წაშლა ?</string>
|
||||
<string name="lbl_edit">ჩასწორება</string>
|
||||
<string name="lbl_search">ძებნა</string>
|
||||
<string name="lbl_filter">ფილტრი</string>
|
||||
<string name="lbl_filter_all">ყველა</string>
|
||||
<string name="lbl_name">სახელი</string>
|
||||
<string name="lbl_rename_playlist">დასაკრავი სიის სახელის შეცვლა</string>
|
||||
<string name="lbl_date">თარიღი</string>
|
||||
<string name="lbl_duration">ხანგრძლივობა</string>
|
||||
<string name="lbl_date_added">დამატების თარიღი</string>
|
||||
<string name="lbl_sort">დაალაგე</string>
|
||||
<string name="lbl_song_count">სიმღერების რაოდენობა</string>
|
||||
<string name="lbl_sort_mode">დაალაგე</string>
|
||||
<string name="lbl_sort_asc">ზრდადობით</string>
|
||||
<string name="lbl_sort_dsc">კლებადობით</string>
|
||||
<string name="lbl_sort_direction">მიხედვით</string>
|
||||
<string name="lbl_play">დაკვრა</string>
|
||||
<string name="lbl_playback">ეხლა იკვრება</string>
|
||||
<string name="lbl_play_next">შემდეგი</string>
|
||||
<string name="lbl_playlist_add">დამკვრელი სიაში დამატება</string>
|
||||
<string name="lbl_artist_details">არტისტის ნახვა</string>
|
||||
<string name="lbl_album_details">ალბომის ნახვა</string>
|
||||
<string name="lbl_queue">რიგი</string>
|
||||
<string name="lbl_shuffle">შემთხვევითი მუსიკა</string>
|
||||
<string name="lbl_share">გაზიარება</string>
|
||||
<string name="lbl_format">ფორმატი</string>
|
||||
<string name="lbl_parent_detail">ნახვა</string>
|
||||
<string name="lbl_props">სიმღერის პარამეტრები</string>
|
||||
<string name="lbl_song_detail">პარამეტრების ნახვა</string>
|
||||
<string name="lbl_size">ზომა</string>
|
||||
<string name="lbl_add">დამატება</string>
|
||||
<string name="lbl_more">მეტი</string>
|
||||
<string name="lbl_cancel">გაუქმება</string>
|
||||
<string name="lbl_save">შენახვა</string>
|
||||
<string name="lbl_version">ვერსია</string>
|
||||
<string name="lbl_about">აპლიკაციის შესახებ</string>
|
||||
<string name="lbl_copied">დაკოპირებულია</string>
|
||||
<string name="lbl_author">ავტორი</string>
|
||||
<string name="lbl_error_info">ინფორმაცია</string>
|
||||
<string name="lbl_feedback">უკუკავშირი</string>
|
||||
<string name="lbl_email">ელექტრონული შეტყობინების გაგზავნა</string>
|
||||
<string name="lbl_donate">დონაცია</string>
|
||||
<string name="lng_indexing">სიმღერების ჩატვირთვა…</string>
|
||||
<string name="lng_queue_added">დაამატე რიგში</string>
|
||||
<string name="lng_playlist_imported">დასაკრავი სია იმპორტირებულია</string>
|
||||
<string name="lng_playlist_renamed">დასაკრავი სიის სახელი შეცვლილია</string>
|
||||
<string name="lng_search_library">ძებნა თქვენს ბიბლიოთეკაში…</string>
|
||||
<string name="lng_empty_albums">ალბომები აქ გამოჩნდება.</string>
|
||||
<string name="lng_empty_artists">არტისტები აქ გამოჩნდება.</string>
|
||||
<string name="set_theme">თემა</string>
|
||||
<string name="set_theme_day">ღია</string>
|
||||
<string name="set_theme_night">მუქი</string>
|
||||
<string name="set_black_mode">შავი თემა</string>
|
||||
<string name="set_action_mode_next">შემდეგზე გადასვლა</string>
|
||||
<string name="set_play_song_from_all">ყველა სიმღერის დაკვრა</string>
|
||||
<string name="lng_playlist_deleted">დასაკრავი სია წაშლილია</string>
|
||||
<string name="lng_playlist_exported">დასაკრავი სია ექსპორტირებულია</string>
|
||||
<string name="lng_playlist_created">დასაკრავი სია შექმნილია</string>
|
||||
<string name="lng_playlist_added">დამატებულია დასაკრავი სიაში</string>
|
||||
<string name="set_theme_auto">ავტომატური</string>
|
||||
<string name="lng_empty_songs">სიმღერები აქ გამოჩნდება.</string>
|
||||
<string name="set_root_title">პარამეტრები</string>
|
||||
<string name="set_accent">ფერები</string>
|
||||
<string name="set_observing">ავტომატური ჩატვირთვა</string>
|
||||
<string name="set_images">სურათები</string>
|
||||
<string name="set_locations">მუსიკის საქაღალდე</string>
|
||||
<string name="set_locations_list">საქაღალდეები</string>
|
||||
<string name="set_locations_new">ახალი საქაღალდე</string>
|
||||
<string name="err_no_music">სიმღერები ვერ მოიძებნა</string>
|
||||
<string name="desc_skip_prev">ბოლო სიმღერაზე გადასვლა</string>
|
||||
</resources>
|
||||
|
|
|
@ -335,4 +335,4 @@
|
|||
<string name="lng_empty_artists">Tutaj pojawią się dodani artyści.</string>
|
||||
<string name="lng_empty_playlists">Tutaj pojawią się dodane playlisty.</string>
|
||||
<string name="lng_empty_genres">Tutaj pojawią się dodane gatunki.</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<resources>
|
||||
<!-- Label Namespace | Static Labels -->
|
||||
<string name="lbl_retry">Tentar novamente</string>
|
||||
<string name="lbl_grant">Confirmar</string>
|
||||
<string name="lbl_grant">Conceder</string>
|
||||
<string name="lbl_genres">Gêneros</string>
|
||||
<string name="lbl_artists">Artistas</string>
|
||||
<string name="lbl_albums">Álbuns</string>
|
||||
|
@ -91,7 +91,7 @@
|
|||
<string name="set_reindex">Recarregar música</string>
|
||||
<string name="set_rewind_prev_desc">Retroceder a música antes de voltar para a anterior</string>
|
||||
<string name="err_no_perms">O Auxio precisa de permissão para ler sua biblioteca de músicas</string>
|
||||
<string name="info_app_desc">Um reprodutor de música simples e racional para android.</string>
|
||||
<string name="info_app_desc">Um reprodutor de música simples e racional para Android.</string>
|
||||
<string name="lng_indexing">Carregando a sua biblioteca de músicas…</string>
|
||||
<string name="lbl_date">Ano</string>
|
||||
<string name="lbl_duration">Duração</string>
|
||||
|
@ -167,17 +167,17 @@
|
|||
<string name="lbl_album_live">Álbum ao vivo</string>
|
||||
<string name="lbl_soundtracks">Trilhas sonoras</string>
|
||||
<string name="lbl_soundtrack">Trilha sonora</string>
|
||||
<string name="lbl_album_remix">Álbum remix</string>
|
||||
<string name="lbl_album_remix">Álbum de remix</string>
|
||||
<string name="lbl_ep_live">EP ao vivo</string>
|
||||
<string name="lbl_ep_remix">Álbum de Remix</string>
|
||||
<string name="lbl_ep_remix">EP de remix</string>
|
||||
<string name="lbl_single_live">Single ao vivo</string>
|
||||
<string name="lng_observing">Monitorando alterações na sua biblioteca de músicas…</string>
|
||||
<string name="set_lib_tabs">Abas da biblioteca</string>
|
||||
<string name="lbl_genre">Gênero</string>
|
||||
<string name="set_play_song_from_artist">Reproduzir do artista</string>
|
||||
<string name="set_pre_amp_with">Ajuste em faixas com metadados</string>
|
||||
<string name="lbl_indexer">Carregando música</string>
|
||||
<string name="lbl_indexing">Carregando música</string>
|
||||
<string name="lbl_indexer">Carregamento de músicas</string>
|
||||
<string name="lbl_indexing">Carregando músicas</string>
|
||||
<string name="lbl_observing">Monitorando a biblioteca de músicas</string>
|
||||
<string name="set_round_mode">Cantos arredondados</string>
|
||||
<string name="set_action_mode_next">Pular para o próximo</string>
|
||||
|
@ -219,9 +219,9 @@
|
|||
<string name="set_exclude_non_music_desc">Ignora arquivos de áudio que não sejam música, como podcasts</string>
|
||||
<string name="set_separators_warning">Aviso: Usar essa configuração pode resultar em alguns metadados serem interpretadas incorretamente como tendo múltiplos valores. Você pode resolver isso pré-definindo caracteres de separador indesejados com uma barra invertida (\\).</string>
|
||||
<string name="set_exclude_non_music">Ignorar arquivos que não sejam músicas</string>
|
||||
<string name="set_cover_mode">Capas de álbuns</string>
|
||||
<string name="set_cover_mode_off">Desligado</string>
|
||||
<string name="set_cover_mode_balanced">Rápido</string>
|
||||
<string name="set_cover_mode">Qualidade das capas de álbuns</string>
|
||||
<string name="set_cover_mode_off">Desativado</string>
|
||||
<string name="set_cover_mode_balanced">Equilibrado</string>
|
||||
<string name="set_cover_mode_high_quality">Alta qualidade</string>
|
||||
<string name="lbl_mixes">Mixagens de DJ</string>
|
||||
<string name="lbl_mix">Mixagem de DJ</string>
|
||||
|
@ -332,6 +332,8 @@
|
|||
<string name="lng_empty_artists">Os seus artistas aparecerão aqui.</string>
|
||||
<string name="lng_empty_playlists">As suas playlists aparecerão aqui.</string>
|
||||
<string name="lng_empty_genres">Os seus gêneros aparecerão aqui.</string>
|
||||
<string name="set_cover_mode_save_space">Salvar espaço</string>
|
||||
<string name="set_cover_mode_save_space">Economizar espaço</string>
|
||||
<string name="set_locations_new">Nova pasta</string>
|
||||
</resources>
|
||||
<string name="set_with_hidden_desc">Ignorar arquivos e pastas que estão ocultos (por exemplo, .cache)</string>
|
||||
<string name="set_with_hidden">Ignorar arquivos ocultos</string>
|
||||
</resources>
|
||||
|
|
145
app/src/main/res/values/colors_ui_black.xml
Normal file
|
@ -0,0 +1,145 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="red_surface_black">#000000</color>
|
||||
<color name="red_surfaceDim_black">#000000</color>
|
||||
<color name="red_surfaceBright_black">#211b1a</color>
|
||||
<color name="red_surfaceContainerLowest_black">#000000</color>
|
||||
<color name="red_surfaceContainerLow_black">#110c0c</color>
|
||||
<color name="red_surfaceContainer_black">#130e0e</color>
|
||||
<color name="red_surfaceContainerHigh_black">#181413</color>
|
||||
<color name="red_surfaceContainerHighest_black">#1e1918</color>
|
||||
|
||||
<color name="pink_surface_black">#000000</color>
|
||||
<color name="pink_surfaceDim_black">#000000</color>
|
||||
<color name="pink_surfaceBright_black">#201b1b</color>
|
||||
<color name="pink_surfaceContainerLowest_black">#000000</color>
|
||||
<color name="pink_surfaceContainerLow_black">#110c0d</color>
|
||||
<color name="pink_surfaceContainer_black">#130e0f</color>
|
||||
<color name="pink_surfaceContainerHigh_black">#181414</color>
|
||||
<color name="pink_surfaceContainerHighest_black">#1e1819</color>
|
||||
|
||||
<color name="purple_surface_black">#000000</color>
|
||||
<color name="purple_surfaceDim_black">#000000</color>
|
||||
<color name="purple_surfaceBright_black">#1f1b1e</color>
|
||||
<color name="purple_surfaceContainerLowest_black">#000000</color>
|
||||
<color name="purple_surfaceContainerLow_black">#0f0c0f</color>
|
||||
<color name="purple_surfaceContainer_black">#110e11</color>
|
||||
<color name="purple_surfaceContainerHigh_black">#171416</color>
|
||||
<color name="purple_surfaceContainerHighest_black">#1c191c</color>
|
||||
|
||||
<color name="deep_purple_surface_black">#000000</color>
|
||||
<color name="deep_purple_surfaceDim_black">#000000</color>
|
||||
<color name="deep_purple_surfaceBright_black">#1d1c1f</color>
|
||||
<color name="deep_purple_surfaceContainerLowest_black">#000000</color>
|
||||
<color name="deep_purple_surfaceContainerLow_black">#0e0d10</color>
|
||||
<color name="deep_purple_surfaceContainer_black">#100f12</color>
|
||||
<color name="deep_purple_surfaceContainerHigh_black">#161417</color>
|
||||
<color name="deep_purple_surfaceContainerHighest_black">#1b1a1d</color>
|
||||
|
||||
<color name="indigo_surface_black">#000000</color>
|
||||
<color name="indigo_surfaceDim_black">#000000</color>
|
||||
<color name="indigo_surfaceBright_black">#1c1c1f</color>
|
||||
<color name="indigo_surfaceContainerLowest_black">#000000</color>
|
||||
<color name="indigo_surfaceContainerLow_black">#0d0d10</color>
|
||||
<color name="indigo_surfaceContainer_black">#0f0f12</color>
|
||||
<color name="indigo_surfaceContainerHigh_black">#141417</color>
|
||||
<color name="indigo_surfaceContainerHighest_black">#1a1a1d</color>
|
||||
|
||||
<color name="blue_surface_black">#000000</color>
|
||||
<color name="blue_surfaceDim_black">#000000</color>
|
||||
<color name="blue_surfaceBright_black">#1b1c1e</color>
|
||||
<color name="blue_surfaceContainerLowest_black">#000000</color>
|
||||
<color name="blue_surfaceContainerLow_black">#0c0d0f</color>
|
||||
<color name="blue_surfaceContainer_black">#0e1012</color>
|
||||
<color name="blue_surfaceContainerHigh_black">#131517</color>
|
||||
<color name="blue_surfaceContainerHighest_black">#191a1c</color>
|
||||
|
||||
<color name="deep_blue_surface_black">#000000</color>
|
||||
<color name="deep_blue_surfaceDim_black">#000000</color>
|
||||
<color name="deep_blue_surfaceBright_black">#1a1d1e</color>
|
||||
<color name="deep_blue_surfaceContainerLowest_black">#000000</color>
|
||||
<color name="deep_blue_surfaceContainerLow_black">#0b0e0f</color>
|
||||
<color name="deep_blue_surfaceContainer_black">#0d1011</color>
|
||||
<color name="deep_blue_surfaceContainerHigh_black">#121517</color>
|
||||
<color name="deep_blue_surfaceContainerHighest_black">#181a1c</color>
|
||||
|
||||
<color name="cyan_surface_black">#000000</color>
|
||||
<color name="cyan_surfaceDim_black">#000000</color>
|
||||
<color name="cyan_surfaceBright_black">#1a1d1e</color>
|
||||
<color name="cyan_surfaceContainerLowest_black">#000000</color>
|
||||
<color name="cyan_surfaceContainerLow_black">#0b0e0e</color>
|
||||
<color name="cyan_surfaceContainer_black">#0d1010</color>
|
||||
<color name="cyan_surfaceContainerHigh_black">#121515</color>
|
||||
<color name="cyan_surfaceContainerHighest_black">#181b1b</color>
|
||||
|
||||
<color name="teal_surface_black">#000000</color>
|
||||
<color name="teal_surfaceDim_black">#000000</color>
|
||||
<color name="teal_surfaceBright_black">#1a1d1c</color>
|
||||
<color name="teal_surfaceContainerLowest_black">#000000</color>
|
||||
<color name="teal_surfaceContainerLow_black">#0b0e0d</color>
|
||||
<color name="teal_surfaceContainer_black">#0d100f</color>
|
||||
<color name="teal_surfaceContainerHigh_black">#121514</color>
|
||||
<color name="teal_surfaceContainerHighest_black">#181b1a</color>
|
||||
|
||||
<color name="green_surface_black">#000000</color>
|
||||
<color name="green_surfaceDim_black">#000000</color>
|
||||
<color name="green_surfaceBright_black">#1b1d1a</color>
|
||||
<color name="green_surfaceContainerLowest_black">#000000</color>
|
||||
<color name="green_surfaceContainerLow_black">#0c0e0b</color>
|
||||
<color name="green_surfaceContainer_black">#0e100d</color>
|
||||
<color name="green_surfaceContainerHigh_black">#131512</color>
|
||||
<color name="green_surfaceContainerHighest_black">#191b18</color>
|
||||
|
||||
<color name="deep_green_surface_black">#000000</color>
|
||||
<color name="deep_green_surfaceDim_black">#000000</color>
|
||||
<color name="deep_green_surfaceBright_black">#1b1d19</color>
|
||||
<color name="deep_green_surfaceContainerLowest_black">#000000</color>
|
||||
<color name="deep_green_surfaceContainerLow_black">#0d0e0a</color>
|
||||
<color name="deep_green_surfaceContainer_black">#0f100d</color>
|
||||
<color name="deep_green_surfaceContainerHigh_black">#141512</color>
|
||||
<color name="deep_green_surfaceContainerHighest_black">#191b16</color>
|
||||
|
||||
<color name="lime_surface_black">#000000</color>
|
||||
<color name="lime_surfaceDim_black">#000000</color>
|
||||
<color name="lime_surfaceBright_black">#1c1d18</color>
|
||||
<color name="lime_surfaceContainerLowest_black">#000000</color>
|
||||
<color name="lime_surfaceContainerLow_black">#0d0e09</color>
|
||||
<color name="lime_surfaceContainer_black">#10100c</color>
|
||||
<color name="lime_surfaceContainerHigh_black">#141511</color>
|
||||
<color name="lime_surfaceContainerHighest_black">#1a1a16</color>
|
||||
|
||||
<color name="yellow_surface_black">#000000</color>
|
||||
<color name="yellow_surfaceDim_black">#000000</color>
|
||||
<color name="yellow_surfaceBright_black">#1f1c17</color>
|
||||
<color name="yellow_surfaceContainerLowest_black">#000000</color>
|
||||
<color name="yellow_surfaceContainerLow_black">#100d09</color>
|
||||
<color name="yellow_surfaceContainer_black">#120f0b</color>
|
||||
<color name="yellow_surfaceContainerHigh_black">#171410</color>
|
||||
<color name="yellow_surfaceContainerHighest_black">#1d1a15</color>
|
||||
|
||||
<color name="orange_surface_black">#000000</color>
|
||||
<color name="orange_surfaceDim_black">#000000</color>
|
||||
<color name="orange_surfaceBright_black">#201b18</color>
|
||||
<color name="orange_surfaceContainerLowest_black">#000000</color>
|
||||
<color name="orange_surfaceContainerLow_black">#110d0a</color>
|
||||
<color name="orange_surfaceContainer_black">#130f0c</color>
|
||||
<color name="orange_surfaceContainerHigh_black">#181411</color>
|
||||
<color name="orange_surfaceContainerHighest_black">#1d1916</color>
|
||||
<color name="brown_surface_black">#000000</color>
|
||||
<color name="brown_surfaceDim_black">#000000</color>
|
||||
<color name="brown_surfaceBright_black">#1e1c1b</color>
|
||||
<color name="brown_surfaceContainerLowest_black">#000000</color>
|
||||
<color name="brown_surfaceContainerLow_black">#0f0d0d</color>
|
||||
<color name="brown_surfaceContainer_black">#110f0f</color>
|
||||
<color name="brown_surfaceContainerHigh_black">#161414</color>
|
||||
<color name="brown_surfaceContainerHighest_black">#1b1a19</color>
|
||||
|
||||
<color name="grey_surface_black">#000000</color>
|
||||
<color name="grey_surfaceDim_black">#000000</color>
|
||||
<color name="grey_surfaceBright_black">#1d1c1c</color>
|
||||
<color name="grey_surfaceContainerLowest_black">#000000</color>
|
||||
<color name="grey_surfaceContainerLow_black">#0e0d0d</color>
|
||||
<color name="grey_surfaceContainer_black">#100f0f</color>
|
||||
<color name="grey_surfaceContainerHigh_black">#151515</color>
|
||||
<color name="grey_surfaceContainerHighest_black">#1a1a1a</color>
|
||||
</resources>
|
|
@ -18,6 +18,7 @@
|
|||
<string name="set_key_square_covers" translatable="false">auxio_square_covers</string>
|
||||
<string name="set_key_music_dirs_include" translatable="false">auxio_include_dirs</string>
|
||||
<string name="set_key_exclude_non_music" translatable="false">auxio_exclude_non_music</string>
|
||||
<string name="set_key_with_hidden" translatable="false">auxio_with_hidden</string>
|
||||
<string name="set_key_music_locations" translatable="false">auxio_music_locations2</string>
|
||||
<string name="set_key_separators" translatable="false">auxio_separators</string>
|
||||
<string name="set_key_auto_sort_names" translatable="false">auxio_auto_sort_names</string>
|
||||
|
@ -78,6 +79,7 @@
|
|||
<item>@string/set_cover_mode_save_space</item>
|
||||
<item>@string/set_cover_mode_balanced</item>
|
||||
<item>@string/set_cover_mode_high_quality</item>
|
||||
<item>@string/set_cover_mode_as_is</item>
|
||||
</string-array>
|
||||
|
||||
<integer-array name="values_cover_mode">
|
||||
|
@ -85,6 +87,7 @@
|
|||
<item>@integer/cover_mode_save_space</item>
|
||||
<item>@integer/cover_mode_balanced</item>
|
||||
<item>@integer/cover_mode_high_quality</item>
|
||||
<item>@integer/cover_mode_as_is</item>
|
||||
</integer-array>
|
||||
|
||||
<string-array name="entries_bar_action">
|
||||
|
@ -181,4 +184,5 @@
|
|||
<integer name="cover_mode_save_space">0xA125</integer>
|
||||
<integer name="cover_mode_balanced">0xA11D</integer>
|
||||
<integer name="cover_mode_high_quality">0xA11E</integer>
|
||||
<integer name="cover_mode_as_is">0xA126</integer>
|
||||
</resources>
|
|
@ -267,6 +267,8 @@
|
|||
<string name="set_observing_desc">Reload the music library whenever it changes (requires persistent notification)</string>
|
||||
<string name="set_exclude_non_music">Exclude non-music</string>
|
||||
<string name="set_exclude_non_music_desc">Ignore audio files that are not music, such as podcasts</string>
|
||||
<string name="set_with_hidden">Include hidden files</string>
|
||||
<string name="set_with_hidden_desc">Include files and folders that are hidden (ex. .cache)</string>
|
||||
<string name="set_separators">Multi-value separators</string>
|
||||
<string name="set_separators_desc">Configure characters that denote multiple tag values</string>
|
||||
<string name="set_separators_comma">Comma (,)</string>
|
||||
|
@ -285,6 +287,7 @@
|
|||
<string name="set_cover_mode_save_space">Save space</string>
|
||||
<string name="set_cover_mode_balanced">Balanced</string>
|
||||
<string name="set_cover_mode_high_quality">High quality</string>
|
||||
<string name="set_cover_mode_as_is">As is</string>
|
||||
<string name="set_square_covers">Force square album covers</string>
|
||||
<string name="set_square_covers_desc">Crop all album covers to a 1:1 aspect ratio</string>
|
||||
|
||||
|
|
|
@ -1,71 +1,173 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<resources>
|
||||
|
||||
<style name="Theme.Auxio.Black" parent="Theme.Auxio.Base">
|
||||
<item name="colorSurface">@android:color/black</item>
|
||||
<item name="colorSurfaceDim">@color/m3_ref_palette_dynamic_neutral_variant4</item>
|
||||
<item name="colorSurfaceBright">@color/m3_ref_palette_dynamic_neutral_variant12</item>
|
||||
<item name="colorSurfaceContainerLowest">@color/m3_ref_palette_dynamic_neutral_variant0</item>
|
||||
<item name="colorSurfaceContainerLow">@color/m3_ref_palette_dynamic_neutral_variant4</item>
|
||||
<item name="colorSurfaceContainer">@color/m3_ref_palette_dynamic_neutral_variant6</item>
|
||||
<item name="colorSurfaceContainerHigh">@color/m3_ref_palette_dynamic_neutral_variant10</item>
|
||||
<item name="colorSurfaceContainerHighest">@color/m3_ref_palette_dynamic_neutral_variant12</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Auxio.Black.Red" parent="Theme.Auxio.Red">
|
||||
<item name="colorSurface">@android:color/black</item>
|
||||
<style name="Theme.Auxio.Red.Black" parent="Theme.Auxio.Red">
|
||||
<item name="colorSurface">@color/red_surface_black</item>
|
||||
<item name="colorSurfaceDim">@color/red_surfaceDim_black</item>
|
||||
<item name="colorSurfaceBright">@color/red_surfaceBright_black</item>
|
||||
<item name="colorSurfaceContainerLowest">@color/red_surfaceContainerLowest_black</item>
|
||||
<item name="colorSurfaceContainerLow">@color/red_surfaceContainerLow_black</item>
|
||||
<item name="colorSurfaceContainer">@color/red_surfaceContainer_black</item>
|
||||
<item name="colorSurfaceContainerHigh">@color/red_surfaceContainerHigh_black</item>
|
||||
<item name="colorSurfaceContainerHighest">@color/red_surfaceContainerHighest_black</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Auxio.Black.Pink" parent="Theme.Auxio.Pink">
|
||||
<item name="colorSurface">@android:color/black</item>
|
||||
<style name="Theme.Auxio.Pink.Black" parent="Theme.Auxio.Pink">
|
||||
<item name="colorSurface">@color/pink_surface_black</item>
|
||||
<item name="colorSurfaceDim">@color/pink_surfaceDim_black</item>
|
||||
<item name="colorSurfaceBright">@color/pink_surfaceBright_black</item>
|
||||
<item name="colorSurfaceContainerLowest">@color/pink_surfaceContainerLowest_black</item>
|
||||
<item name="colorSurfaceContainerLow">@color/pink_surfaceContainerLow_black</item>
|
||||
<item name="colorSurfaceContainer">@color/pink_surfaceContainer_black</item>
|
||||
<item name="colorSurfaceContainerHigh">@color/pink_surfaceContainerHigh_black</item>
|
||||
<item name="colorSurfaceContainerHighest">@color/pink_surfaceContainerHighest_black</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Auxio.Black.Purple" parent="Theme.Auxio.Purple">
|
||||
<item name="colorSurface">@android:color/black</item>
|
||||
<style name="Theme.Auxio.Purple.Black" parent="Theme.Auxio.Purple">
|
||||
<item name="colorSurface">@color/purple_surface_black</item>
|
||||
<item name="colorSurfaceDim">@color/purple_surfaceDim_black</item>
|
||||
<item name="colorSurfaceBright">@color/purple_surfaceBright_black</item>
|
||||
<item name="colorSurfaceContainerLowest">@color/purple_surfaceContainerLowest_black</item>
|
||||
<item name="colorSurfaceContainerLow">@color/purple_surfaceContainerLow_black</item>
|
||||
<item name="colorSurfaceContainer">@color/purple_surfaceContainer_black</item>
|
||||
<item name="colorSurfaceContainerHigh">@color/purple_surfaceContainerHigh_black</item>
|
||||
<item name="colorSurfaceContainerHighest">@color/purple_surfaceContainerHighest_black</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Auxio.Black.DeepPurple" parent="Theme.Auxio.DeepPurple">
|
||||
<item name="colorSurface">@android:color/black</item>
|
||||
<style name="Theme.Auxio.DeepPurple.Black" parent="Theme.Auxio.DeepPurple">
|
||||
<item name="colorSurface">@color/deep_purple_surface_black</item>
|
||||
<item name="colorSurfaceDim">@color/deep_purple_surfaceDim_black</item>
|
||||
<item name="colorSurfaceBright">@color/deep_purple_surfaceBright_black</item>
|
||||
<item name="colorSurfaceContainerLowest">@color/deep_purple_surfaceContainerLowest_black</item>
|
||||
<item name="colorSurfaceContainerLow">@color/deep_purple_surfaceContainerLow_black</item>
|
||||
<item name="colorSurfaceContainer">@color/deep_purple_surfaceContainer_black</item>
|
||||
<item name="colorSurfaceContainerHigh">@color/deep_purple_surfaceContainerHigh_black</item>
|
||||
<item name="colorSurfaceContainerHighest">@color/deep_purple_surfaceContainerHighest_black</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Auxio.Black.Indigo" parent="Theme.Auxio.Indigo">
|
||||
<item name="colorSurface">@android:color/black</item>
|
||||
<style name="Theme.Auxio.Indigo.Black" parent="Theme.Auxio.Indigo">
|
||||
<item name="colorSurface">@color/indigo_surface_black</item>
|
||||
<item name="colorSurfaceDim">@color/indigo_surfaceDim_black</item>
|
||||
<item name="colorSurfaceBright">@color/indigo_surfaceBright_black</item>
|
||||
<item name="colorSurfaceContainerLowest">@color/indigo_surfaceContainerLowest_black</item>
|
||||
<item name="colorSurfaceContainerLow">@color/indigo_surfaceContainerLow_black</item>
|
||||
<item name="colorSurfaceContainer">@color/indigo_surfaceContainer_black</item>
|
||||
<item name="colorSurfaceContainerHigh">@color/indigo_surfaceContainerHigh_black</item>
|
||||
<item name="colorSurfaceContainerHighest">@color/indigo_surfaceContainerHighest_black</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Auxio.Black.Blue" parent="Theme.Auxio.Blue">
|
||||
<item name="colorSurface">@android:color/black</item>
|
||||
<style name="Theme.Auxio.Blue.Black" parent="Theme.Auxio.Blue">
|
||||
<item name="colorSurface">@color/blue_surface_black</item>
|
||||
<item name="colorSurfaceDim">@color/blue_surfaceDim_black</item>
|
||||
<item name="colorSurfaceBright">@color/blue_surfaceBright_black</item>
|
||||
<item name="colorSurfaceContainerLowest">@color/blue_surfaceContainerLowest_black</item>
|
||||
<item name="colorSurfaceContainerLow">@color/blue_surfaceContainerLow_black</item>
|
||||
<item name="colorSurfaceContainer">@color/blue_surfaceContainer_black</item>
|
||||
<item name="colorSurfaceContainerHigh">@color/blue_surfaceContainerHigh_black</item>
|
||||
<item name="colorSurfaceContainerHighest">@color/blue_surfaceContainerHighest_black</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Auxio.Black.DeepBlue" parent="Theme.Auxio.DeepBlue">
|
||||
<item name="colorSurface">@android:color/black</item>
|
||||
<style name="Theme.Auxio.DeepBlue.Black" parent="Theme.Auxio.DeepBlue">
|
||||
<item name="colorSurface">@color/deep_blue_surface_black</item>
|
||||
<item name="colorSurfaceDim">@color/deep_blue_surfaceDim_black</item>
|
||||
<item name="colorSurfaceBright">@color/deep_blue_surfaceBright_black</item>
|
||||
<item name="colorSurfaceContainerLowest">@color/deep_blue_surfaceContainerLowest_black</item>
|
||||
<item name="colorSurfaceContainerLow">@color/deep_blue_surfaceContainerLow_black</item>
|
||||
<item name="colorSurfaceContainer">@color/deep_blue_surfaceContainer_black</item>
|
||||
<item name="colorSurfaceContainerHigh">@color/deep_blue_surfaceContainerHigh_black</item>
|
||||
<item name="colorSurfaceContainerHighest">@color/deep_blue_surfaceContainerHighest_black</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Auxio.Black.Cyan" parent="Theme.Auxio.Cyan">
|
||||
<item name="colorSurface">@android:color/black</item>
|
||||
<style name="Theme.Auxio.Cyan.Black" parent="Theme.Auxio.Cyan">
|
||||
<item name="colorSurface">@color/cyan_surface_black</item>
|
||||
<item name="colorSurfaceDim">@color/cyan_surfaceDim_black</item>
|
||||
<item name="colorSurfaceBright">@color/cyan_surfaceBright_black</item>
|
||||
<item name="colorSurfaceContainerLowest">@color/cyan_surfaceContainerLowest_black</item>
|
||||
<item name="colorSurfaceContainerLow">@color/cyan_surfaceContainerLow_black</item>
|
||||
<item name="colorSurfaceContainer">@color/cyan_surfaceContainer_black</item>
|
||||
<item name="colorSurfaceContainerHigh">@color/cyan_surfaceContainerHigh_black</item>
|
||||
<item name="colorSurfaceContainerHighest">@color/cyan_surfaceContainerHighest_black</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Auxio.Black.Teal" parent="Theme.Auxio.Teal">
|
||||
<item name="colorSurface">@android:color/black</item>
|
||||
<style name="Theme.Auxio.Teal.Black" parent="Theme.Auxio.Teal">
|
||||
<item name="colorSurface">@color/teal_surface_black</item>
|
||||
<item name="colorSurfaceDim">@color/teal_surfaceDim_black</item>
|
||||
<item name="colorSurfaceBright">@color/teal_surfaceBright_black</item>
|
||||
<item name="colorSurfaceContainerLowest">@color/teal_surfaceContainerLowest_black</item>
|
||||
<item name="colorSurfaceContainerLow">@color/teal_surfaceContainerLow_black</item>
|
||||
<item name="colorSurfaceContainer">@color/teal_surfaceContainer_black</item>
|
||||
<item name="colorSurfaceContainerHigh">@color/teal_surfaceContainerHigh_black</item>
|
||||
<item name="colorSurfaceContainerHighest">@color/teal_surfaceContainerHighest_black</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Auxio.Black.Green" parent="Theme.Auxio.Green">
|
||||
<item name="colorSurface">@android:color/black</item>
|
||||
<style name="Theme.Auxio.Green.Black" parent="Theme.Auxio.Green">
|
||||
<item name="colorSurface">@color/green_surface_black</item>
|
||||
<item name="colorSurfaceDim">@color/green_surfaceDim_black</item>
|
||||
<item name="colorSurfaceBright">@color/green_surfaceBright_black</item>
|
||||
<item name="colorSurfaceContainerLowest">@color/green_surfaceContainerLowest_black</item>
|
||||
<item name="colorSurfaceContainerLow">@color/green_surfaceContainerLow_black</item>
|
||||
<item name="colorSurfaceContainer">@color/green_surfaceContainer_black</item>
|
||||
<item name="colorSurfaceContainerHigh">@color/green_surfaceContainerHigh_black</item>
|
||||
<item name="colorSurfaceContainerHighest">@color/green_surfaceContainerHighest_black</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Auxio.Black.DeepGreen" parent="Theme.Auxio.DeepGreen">
|
||||
<item name="colorSurface">@android:color/black</item>
|
||||
<style name="Theme.Auxio.DeepGreen.Black" parent="Theme.Auxio.DeepGreen">
|
||||
<item name="colorSurface">@color/deep_green_surface_black</item>
|
||||
<item name="colorSurfaceDim">@color/deep_green_surfaceDim_black</item>
|
||||
<item name="colorSurfaceBright">@color/deep_green_surfaceBright_black</item>
|
||||
<item name="colorSurfaceContainerLowest">@color/deep_green_surfaceContainerLowest_black</item>
|
||||
<item name="colorSurfaceContainerLow">@color/deep_green_surfaceContainerLow_black</item>
|
||||
<item name="colorSurfaceContainer">@color/deep_green_surfaceContainer_black</item>
|
||||
<item name="colorSurfaceContainerHigh">@color/deep_green_surfaceContainerHigh_black</item>
|
||||
<item name="colorSurfaceContainerHighest">@color/deep_green_surfaceContainerHighest_black</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Auxio.Black.Lime" parent="Theme.Auxio.Lime">
|
||||
<item name="colorSurface">@android:color/black</item>
|
||||
<style name="Theme.Auxio.Lime.Black" parent="Theme.Auxio.Lime">
|
||||
<item name="colorSurface">@color/lime_surface_black</item>
|
||||
<item name="colorSurfaceDim">@color/lime_surfaceDim_black</item>
|
||||
<item name="colorSurfaceBright">@color/lime_surfaceBright_black</item>
|
||||
<item name="colorSurfaceContainerLowest">@color/lime_surfaceContainerLowest_black</item>
|
||||
<item name="colorSurfaceContainerLow">@color/lime_surfaceContainerLow_black</item>
|
||||
<item name="colorSurfaceContainer">@color/lime_surfaceContainer_black</item>
|
||||
<item name="colorSurfaceContainerHigh">@color/lime_surfaceContainerHigh_black</item>
|
||||
<item name="colorSurfaceContainerHighest">@color/lime_surfaceContainerHighest_black</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Auxio.Black.Yellow" parent="Theme.Auxio.Yellow">
|
||||
<item name="colorSurface">@android:color/black</item>
|
||||
<style name="Theme.Auxio.Yellow.Black" parent="Theme.Auxio.Yellow">
|
||||
<item name="colorSurface">@color/yellow_surface_black</item>
|
||||
<item name="colorSurfaceDim">@color/yellow_surfaceDim_black</item>
|
||||
<item name="colorSurfaceBright">@color/yellow_surfaceBright_black</item>
|
||||
<item name="colorSurfaceContainerLowest">@color/yellow_surfaceContainerLowest_black</item>
|
||||
<item name="colorSurfaceContainerLow">@color/yellow_surfaceContainerLow_black</item>
|
||||
<item name="colorSurfaceContainer">@color/yellow_surfaceContainer_black</item>
|
||||
<item name="colorSurfaceContainerHigh">@color/yellow_surfaceContainerHigh_black</item>
|
||||
<item name="colorSurfaceContainerHighest">@color/yellow_surfaceContainerHighest_black</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Auxio.Black.Orange" parent="Theme.Auxio.Orange">
|
||||
<item name="colorSurface">@android:color/black</item>
|
||||
<style name="Theme.Auxio.Orange.Black" parent="Theme.Auxio.Orange">
|
||||
<item name="colorSurface">@color/orange_surface_black</item>
|
||||
<item name="colorSurfaceDim">@color/orange_surfaceDim_black</item>
|
||||
<item name="colorSurfaceBright">@color/orange_surfaceBright_black</item>
|
||||
<item name="colorSurfaceContainerLowest">@color/orange_surfaceContainerLowest_black</item>
|
||||
<item name="colorSurfaceContainerLow">@color/orange_surfaceContainerLow_black</item>
|
||||
<item name="colorSurfaceContainer">@color/orange_surfaceContainer_black</item>
|
||||
<item name="colorSurfaceContainerHigh">@color/orange_surfaceContainerHigh_black</item>
|
||||
<item name="colorSurfaceContainerHighest">@color/orange_surfaceContainerHighest_black</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Auxio.Black.Brown" parent="Theme.Auxio.Brown">
|
||||
<item name="colorSurface">@android:color/black</item>
|
||||
<style name="Theme.Auxio.Brown.Black" parent="Theme.Auxio.Brown">
|
||||
<item name="colorSurface">@color/brown_surface_black</item>
|
||||
<item name="colorSurfaceDim">@color/brown_surfaceDim_black</item>
|
||||
<item name="colorSurfaceBright">@color/brown_surfaceBright_black</item>
|
||||
<item name="colorSurfaceContainerLowest">@color/brown_surfaceContainerLowest_black</item>
|
||||
<item name="colorSurfaceContainerLow">@color/brown_surfaceContainerLow_black</item>
|
||||
<item name="colorSurfaceContainer">@color/brown_surfaceContainer_black</item>
|
||||
<item name="colorSurfaceContainerHigh">@color/brown_surfaceContainerHigh_black</item>
|
||||
<item name="colorSurfaceContainerHighest">@color/brown_surfaceContainerHighest_black</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Auxio.Black.Grey" parent="Theme.Auxio.Grey">
|
||||
<item name="colorSurface">@android:color/black</item>
|
||||
<style name="Theme.Auxio.Grey.Black" parent="Theme.Auxio.Grey">
|
||||
<item name="colorSurface">@color/grey_surface_black</item>
|
||||
<item name="colorSurfaceDim">@color/grey_surfaceDim_black</item>
|
||||
<item name="colorSurfaceBright">@color/grey_surfaceBright_black</item>
|
||||
<item name="colorSurfaceContainerLowest">@color/grey_surfaceContainerLowest_black</item>
|
||||
<item name="colorSurfaceContainerLow">@color/grey_surfaceContainerLow_black</item>
|
||||
<item name="colorSurfaceContainer">@color/grey_surfaceContainer_black</item>
|
||||
<item name="colorSurfaceContainerHigh">@color/grey_surfaceContainerHigh_black</item>
|
||||
<item name="colorSurfaceContainerHighest">@color/grey_surfaceContainerHighest_black</item>
|
||||
</style>
|
||||
</resources>
|
|
@ -14,6 +14,12 @@
|
|||
app:key="@string/set_key_exclude_non_music"
|
||||
app:summary="@string/set_exclude_non_music_desc"
|
||||
app:title="@string/set_exclude_non_music" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
app:defaultValue="false"
|
||||
app:key="@string/set_key_with_hidden"
|
||||
app:summary="@string/set_with_hidden_desc"
|
||||
app:title="@string/set_with_hidden" />
|
||||
|
||||
<org.oxycblt.auxio.settings.ui.WrappedDialogPreference
|
||||
app:key="@string/set_key_separators"
|
||||
|
@ -50,4 +56,4 @@
|
|||
app:title="@string/set_square_covers" />
|
||||
|
||||
</PreferenceCategory>
|
||||
</PreferenceScreen>
|
||||
</PreferenceScreen>
|
||||
|
|
|
@ -6,8 +6,7 @@ Auxio je lokální hudební přehrávač s rychlým a spolehlivým UI/UX bez spo
|
|||
- Responzivní UI podle nejnovějších pokynů Material Design
|
||||
- Příjemné UX, které upřednostňuje snadné používání před okrajovými případy
|
||||
- Přizpůsobitelné chování
|
||||
- Podpora čísel disků, více interpretů, typů vydání,
|
||||
přesná/původní data, štítky pro řazení a další
|
||||
- Podpora čísel disků, více interpretů, typů vydání, přesných/původních dat, štítků pro řazení a dalších
|
||||
- Pokročilý systém umělců spojující interprety a interprety alb
|
||||
- Správa složek podporující SD karty
|
||||
- Spolehlivá funkce seznamů skladeb
|
||||
|
@ -22,4 +21,4 @@ přesná/původní data, štítky pro řazení a další
|
|||
- Automatické přehrávání při připojení sluchátek
|
||||
- Stylové widgety, které se automaticky adaptují své velikosti
|
||||
- Plně soukromý a offline
|
||||
- Žádné zaoblené obaly alb (ve výchozím nastavení)
|
||||
- Žádné zaoblené obaly alb (pokud je nechcete)
|
||||
|
|
|
@ -22,4 +22,4 @@ Auxio ist ein lokaler Musik-Player mit einer schnellen, verlässlichen UI/UX, ab
|
|||
- Autoplay bei Kopfhörern
|
||||
- Stylische Widgets, die ihre Größe anpassen
|
||||
- vollständig privat und offline
|
||||
- keine abgerundeten Album-Cover (standardmäßig)
|
||||
- keine abgerundeten Album-Cover (wenn du willst)
|
||||
|
|
4
fastlane/metadata/android/en-US/changelogs/61.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
Auxio 4.0.0 completely overhauls the user experience, with a refreshed design based on the latest Material Design specs
|
||||
and a brand new music loader with signifigant improvements to device and tag support.
|
||||
This issue fixes several regressions from v3.6.3 functionality.
|
||||
For more information, see https://github.com/OxygenCobalt/Auxio/releases/tag/v4.0.2.
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
Before Width: | Height: | Size: 323 KiB After Width: | Height: | Size: 323 KiB |
Before Width: | Height: | Size: 265 KiB After Width: | Height: | Size: 265 KiB |
Before Width: | Height: | Size: 196 KiB After Width: | Height: | Size: 196 KiB |
Before Width: | Height: | Size: 262 KiB After Width: | Height: | Size: 262 KiB |
Before Width: | Height: | Size: 188 KiB After Width: | Height: | Size: 188 KiB |
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 151 KiB |
|
@ -6,15 +6,14 @@ Auxio on kohalikus nutiseadmes töötav kiire ja usaldusväärse kasutajaliidese
|
|||
- viimastest Material Designi põhimõtetest lähtuv kiire kasutajaliides
|
||||
- arendajate omal hinnangul põhinev kasutajaliides, mis eelistab kasutuse lihtsust harvaesinevate olukordade lahendamisele
|
||||
- kohandatav käitumine
|
||||
- plaadinumbrite, mitme esitaja, väljaande tüübi,
|
||||
täpsete avaldamise kuupäevade sortimiseks mõeldud siltide ja palju muu sarnase tugi
|
||||
- plaadinumbrite, mitme esitaja, väljaande tüübi, täpsete avaldamise kuupäevade sortimiseks mõeldud siltide ja palju muu sarnase tugi
|
||||
- tavalisest tõhusam esitajate haldus, mis normaliseerib esitajad ning albumi esitajad
|
||||
- kaustade haldus, mis saab hakkama SD-kaartidega
|
||||
- usaldusväärse esitusloendi loomine
|
||||
- taasesituse oleku meeldejätmine
|
||||
- tugi Android Auto liidestusele
|
||||
- automaatne taasesitus lugudevahelise vaikusteta
|
||||
- taasesituse valjuse tundlikkuse tugi (MP3, FLAC, OGG, OPUS ja MP4 failide pihul)
|
||||
- taasesituse valjuse tundlikkuse tugi (MP3, FLAC, OGG, OPUS ja MP4 failide puhul)
|
||||
- välise ekvalaiseri tugi (nt. Wavelet)
|
||||
- äärest-ääreni visuaal
|
||||
- lõimitud albumikaante tugi
|
||||
|
@ -22,4 +21,4 @@ täpsete avaldamise kuupäevade sortimiseks mõeldud siltide ja palju muu sarnas
|
|||
- automaatne taasesitus kõrvaklappidest
|
||||
- stiilsed vidinad, mis automaatselt kohandavad oma suurust
|
||||
- täiesti privaatne ja võrguühendust mittevajav
|
||||
- plaadikaante ümarad nurgad puuduvad (vaikimisi)
|
||||
- plaadikaante ümarad nurgad puuduvad (kui seda eelistad)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
Auxio je lokalni glazbeni player s brzim, pouzdanim UI/UX bez mnogih beskorisnih značajki prisutnih u drugim glazbenim playerima. Izgrađen na osnovi modernih biblioteka za reprodukciju, 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>.
|
||||
Auxio je lokalni glazbeni player s brzim, pouzdanim UI/UX bez mnogih beskorisnih značajki prisutnih u drugim glazbenim playerima. Izgrađen na osnovi modernih biblioteka za reprodukciju, 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>
|
||||
|
||||
|
@ -6,14 +6,12 @@ Auxio je lokalni glazbeni player s brzim, pouzdanim UI/UX bez mnogih beskorisnih
|
|||
- 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
|
||||
- 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 koje podržava SD karticu
|
||||
- Pouzdana funkcija popisa pjesama
|
||||
- Automaska podrška za Android
|
||||
- Automaska podrška za Android Auto
|
||||
- Automatska reprodukcija bez prekida
|
||||
- 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
|
||||
|
@ -22,4 +20,4 @@ precizni/izvorni datumi, sortiranje oznaka i više
|
|||
- Automatska reprodukcija slušalica
|
||||
- Elegantni widgeti koji se automatski prilagođavaju njihovoj veličini
|
||||
- Potpuno privatno i izvan mreže
|
||||
- Bez zaobljenih naslovnica albuma (zadano)
|
||||
- Bez zaobljenih naslovnica albuma (ako ih želite)
|
||||
|
|
|
@ -1 +1 @@
|
|||
Un semplice, razionale lettore musicale
|
||||
Un lettore musicale semplice e razionale
|
||||
|
|
|
@ -21,4 +21,4 @@ Auxio é um reprodutor de música local com uma interface/experiência de usuár
|
|||
- Reprodução automática em fones de ouvido.
|
||||
- Widgets elegantes que se adaptam automaticamente ao tamanho.
|
||||
- Completamente privado e off-line.
|
||||
- Sem capas de álbuns arredondadas (por padrão).
|
||||
- Sem capas de álbuns arredondadas (caso queira).
|
||||
|
|
|
@ -6,8 +6,7 @@ Auxio — це локальний музичний плеєр із швидки
|
|||
- Snappy UI, створений на основі останніх рекомендацій Material Design
|
||||
- Переконливий UX, який надає перевагу простоті використання над крайніми випадками
|
||||
- Настроювана поведінка
|
||||
- Підтримка номерів дисків, кількох виконавців, типів випусків,
|
||||
точні/оригінальні дати, теги сортування тощо
|
||||
- Підтримка номерів дисків, кількох виконавців, типів випусків, точних/оригінальних дат, тегів сортування тощо
|
||||
— Розширена система виконавців, яка об’єднує виконавців і виконавців альбомів
|
||||
- Керування папками з підтримкою SD-карти
|
||||
— Надійна функція списків відтворення
|
||||
|
@ -22,4 +21,4 @@ Auxio — це локальний музичний плеєр із швидки
|
|||
- Автовідтворення гарнітури
|
||||
- Стильні віджети, які автоматично адаптуються до їх розміру
|
||||
- Повністю приватний і офлайн
|
||||
- Немає округлених обкладинок альбомів (за замовчуванням)
|
||||
- Немає закруглених обкладинок альбомів (якщо ви їх хочете)
|
||||
|
|
|
@ -6,8 +6,7 @@ Auxio 是一款本地音乐播放器,它拥有快速、可靠的 UI/UX,没
|
|||
- 源于最新 Material You 设计规范的灵动界面
|
||||
- 优先考虑易用性的独到用户体验
|
||||
- 可定制的播放器行为
|
||||
- 支持唱片编号、多名艺术家、发布类型、精确/原始日期、
|
||||
标签排序及其他更多功能
|
||||
- 支持唱片编号、多名艺术家、发行类型、精确/原始日期、排序标签以及更多
|
||||
- 统一“艺术家”和“专辑艺术家”的高级“艺术家”系统
|
||||
- 文件夹管理功能可以感知到 SD 卡
|
||||
- 可靠的播放列表功能
|
||||
|
@ -22,4 +21,4 @@ Auxio 是一款本地音乐播放器,它拥有快速、可靠的 UI/UX,没
|
|||
- 耳机连接时自动播放
|
||||
- 按桌面尺寸自适应的风格化微件
|
||||
- 完全离线且私密
|
||||
- 没有圆角的专辑封面(默认设置)
|
||||
- 没有圆角的专辑封面(即使你想要)
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
#include "taglib/vorbisfile.h"
|
||||
#include "taglib/wavfile.h"
|
||||
|
||||
bool parseMpeg(const char *name, TagLib::File *file,
|
||||
bool parseMpeg(const std::string &name, TagLib::File *file,
|
||||
JMetadataBuilder &jBuilder) {
|
||||
auto *mpegFile = dynamic_cast<TagLib::MPEG::File*>(file);
|
||||
if (mpegFile == nullptr) {
|
||||
|
@ -41,7 +41,7 @@ bool parseMpeg(const char *name, TagLib::File *file,
|
|||
try {
|
||||
jBuilder.setId3v1(*id3v1Tag);
|
||||
} catch (std::exception &e) {
|
||||
LOGE("Unable to parse ID3v1 tag in %s: %s", name, e.what());
|
||||
LOGE("Unable to parse ID3v1 tag in %s: %s", name.c_str(), e.what());
|
||||
}
|
||||
}
|
||||
auto id3v2Tag = mpegFile->ID3v2Tag();
|
||||
|
@ -49,13 +49,13 @@ bool parseMpeg(const char *name, TagLib::File *file,
|
|||
try {
|
||||
jBuilder.setId3v2(*id3v2Tag);
|
||||
} catch (std::exception &e) {
|
||||
LOGE("Unable to parse ID3v2 tag in %s: %s", name, e.what());
|
||||
LOGE("Unable to parse ID3v2 tag in %s: %s", name.c_str(), e.what());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parseMp4(const char *name, TagLib::File *file,
|
||||
bool parseMp4(const std::string &name, TagLib::File *file,
|
||||
JMetadataBuilder &jBuilder) {
|
||||
auto *mp4File = dynamic_cast<TagLib::MP4::File*>(file);
|
||||
if (mp4File == nullptr) {
|
||||
|
@ -66,13 +66,13 @@ bool parseMp4(const char *name, TagLib::File *file,
|
|||
try {
|
||||
jBuilder.setMp4(*tag);
|
||||
} catch (std::exception &e) {
|
||||
LOGE("Unable to parse MP4 tag in %s: %s", name, e.what());
|
||||
LOGE("Unable to parse MP4 tag in %s: %s", name.c_str(), e.what());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parseFlac(const char *name, TagLib::File *file,
|
||||
bool parseFlac(const std::string &name, TagLib::File *file,
|
||||
JMetadataBuilder &jBuilder) {
|
||||
auto *flacFile = dynamic_cast<TagLib::FLAC::File*>(file);
|
||||
if (flacFile == nullptr) {
|
||||
|
@ -83,7 +83,7 @@ bool parseFlac(const char *name, TagLib::File *file,
|
|||
try {
|
||||
jBuilder.setId3v1(*id3v1Tag);
|
||||
} catch (std::exception &e) {
|
||||
LOGE("Unable to parse ID3v1 tag in %s: %s", name, e.what());
|
||||
LOGE("Unable to parse ID3v1 tag in %s: %s", name.c_str(), e.what());
|
||||
}
|
||||
}
|
||||
auto id3v2Tag = flacFile->ID3v2Tag();
|
||||
|
@ -91,7 +91,7 @@ bool parseFlac(const char *name, TagLib::File *file,
|
|||
try {
|
||||
jBuilder.setId3v2(*id3v2Tag);
|
||||
} catch (std::exception &e) {
|
||||
LOGE("Unable to parse ID3v2 tag in %s: %s", name, e.what());
|
||||
LOGE("Unable to parse ID3v2 tag in %s: %s", name.c_str(), e.what());
|
||||
}
|
||||
}
|
||||
auto xiphComment = flacFile->xiphComment();
|
||||
|
@ -99,7 +99,8 @@ bool parseFlac(const char *name, TagLib::File *file,
|
|||
try {
|
||||
jBuilder.setXiph(*xiphComment);
|
||||
} catch (std::exception &e) {
|
||||
LOGE("Unable to parse Xiph comment in %s: %s", name, e.what());
|
||||
LOGE("Unable to parse Xiph comment in %s: %s", name.c_str(),
|
||||
e.what());
|
||||
}
|
||||
}
|
||||
auto pics = flacFile->pictureList();
|
||||
|
@ -107,7 +108,7 @@ bool parseFlac(const char *name, TagLib::File *file,
|
|||
return true;
|
||||
}
|
||||
|
||||
bool parseOpus(const char *name, TagLib::File *file,
|
||||
bool parseOpus(const std::string &name, TagLib::File *file,
|
||||
JMetadataBuilder &jBuilder) {
|
||||
auto *opusFile = dynamic_cast<TagLib::Ogg::Opus::File*>(file);
|
||||
if (opusFile == nullptr) {
|
||||
|
@ -118,13 +119,14 @@ bool parseOpus(const char *name, TagLib::File *file,
|
|||
try {
|
||||
jBuilder.setXiph(*tag);
|
||||
} catch (std::exception &e) {
|
||||
LOGE("Unable to parse Xiph comment in %s: %s", name, e.what());
|
||||
LOGE("Unable to parse Xiph comment in %s: %s", name.c_str(),
|
||||
e.what());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parseVorbis(const char *name, TagLib::File *file,
|
||||
bool parseVorbis(const std::string &name, TagLib::File *file,
|
||||
JMetadataBuilder &jBuilder) {
|
||||
auto *vorbisFile = dynamic_cast<TagLib::Ogg::Vorbis::File*>(file);
|
||||
if (vorbisFile == nullptr) {
|
||||
|
@ -135,13 +137,13 @@ bool parseVorbis(const char *name, TagLib::File *file,
|
|||
try {
|
||||
jBuilder.setXiph(*tag);
|
||||
} catch (std::exception &e) {
|
||||
LOGE("Unable to parse Xiph comment %s: %s", name, e.what());
|
||||
LOGE("Unable to parse Xiph comment %s: %s", name.c_str(), e.what());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parseWav(const char *name, TagLib::File *file,
|
||||
bool parseWav(const std::string &name, TagLib::File *file,
|
||||
JMetadataBuilder &jBuilder) {
|
||||
auto *wavFile = dynamic_cast<TagLib::RIFF::WAV::File*>(file);
|
||||
if (wavFile == nullptr) {
|
||||
|
@ -152,7 +154,7 @@ bool parseWav(const char *name, TagLib::File *file,
|
|||
try {
|
||||
jBuilder.setId3v2(*tag);
|
||||
} catch (std::exception &e) {
|
||||
LOGE("Unable to parse ID3v2 tag in %s: %s", name, e.what());
|
||||
LOGE("Unable to parse ID3v2 tag in %s: %s", name.c_str(), e.what());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
@ -162,7 +164,7 @@ extern "C" JNIEXPORT jobject JNICALL
|
|||
Java_org_oxycblt_musikr_metadata_TagLibJNI_openNative(JNIEnv *env,
|
||||
jobject /* this */,
|
||||
jobject inputStream) {
|
||||
const char *name = nullptr;
|
||||
std::string name = "unknown file";
|
||||
try {
|
||||
JInputStream jStream {env, inputStream};
|
||||
name = jStream.name();
|
||||
|
@ -189,12 +191,12 @@ Java_org_oxycblt_musikr_metadata_TagLibJNI_openNative(JNIEnv *env,
|
|||
} else if (parseWav(name, file, jBuilder)) {
|
||||
jBuilder.setMimeType("audio/wav");
|
||||
} else {
|
||||
LOGE("File format in %s is not supported", name);
|
||||
LOGE("File format in %s is not supported", name.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
return jBuilder.build();
|
||||
} catch (std::exception &e) {
|
||||
LOGE("Unable to parse metadata in %s: %s", name != nullptr ? name : "unknown file", e.what());
|
||||
LOGE("Unable to parse metadata in %s: %s", name.c_str(), e.what());
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
package org.oxycblt.musikr
|
||||
|
||||
import org.oxycblt.musikr.cache.Cache
|
||||
import org.oxycblt.musikr.cover.Cover
|
||||
import org.oxycblt.musikr.cover.MutableCovers
|
||||
import org.oxycblt.musikr.playlist.db.StoredPlaylists
|
||||
import org.oxycblt.musikr.tag.interpret.Naming
|
||||
|
@ -37,7 +38,7 @@ data class Storage(
|
|||
* with the cache for best performance. This will be used during music loading and when
|
||||
* retrieving cover information from the library.
|
||||
*/
|
||||
val storedCovers: MutableCovers,
|
||||
val storedCovers: MutableCovers<out Cover>,
|
||||
|
||||
/**
|
||||
* A repository of user-created playlists that should also be loaded into the library. This will
|
||||
|
@ -53,5 +54,8 @@ data class Interpretation(
|
|||
val naming: Naming,
|
||||
|
||||
/** What separators delimit multi-value audio tags. */
|
||||
val separators: Separators
|
||||
val separators: Separators,
|
||||
|
||||
/** Whether to ignore hidden files and directories (those starting with a dot). */
|
||||
val ignoreHidden: Boolean
|
||||
)
|
||||
|
|
|
@ -71,7 +71,7 @@ interface Musikr {
|
|||
fun new(context: Context, storage: Storage, interpretation: Interpretation): Musikr =
|
||||
MusikrImpl(
|
||||
storage,
|
||||
ExploreStep.from(context, storage),
|
||||
ExploreStep.from(context, storage, interpretation),
|
||||
ExtractStep.from(context, storage),
|
||||
EvaluateStep.new(storage, interpretation))
|
||||
}
|
||||
|
|
|
@ -18,12 +18,13 @@
|
|||
|
||||
package org.oxycblt.musikr.cache
|
||||
|
||||
import org.oxycblt.musikr.cover.Cover
|
||||
import org.oxycblt.musikr.cover.Covers
|
||||
import org.oxycblt.musikr.fs.DeviceFile
|
||||
import org.oxycblt.musikr.fs.device.DeviceFile
|
||||
import org.oxycblt.musikr.pipeline.RawSong
|
||||
|
||||
abstract class Cache {
|
||||
internal abstract suspend fun read(file: DeviceFile, covers: Covers): CacheResult
|
||||
internal abstract suspend fun read(file: DeviceFile, covers: Covers<out Cover>): CacheResult
|
||||
|
||||
internal abstract suspend fun write(song: RawSong)
|
||||
|
||||
|
|
|
@ -31,9 +31,10 @@ import androidx.room.RoomDatabase
|
|||
import androidx.room.Transaction
|
||||
import androidx.room.TypeConverter
|
||||
import androidx.room.TypeConverters
|
||||
import org.oxycblt.musikr.cover.Cover
|
||||
import org.oxycblt.musikr.cover.CoverResult
|
||||
import org.oxycblt.musikr.cover.Covers
|
||||
import org.oxycblt.musikr.cover.ObtainResult
|
||||
import org.oxycblt.musikr.fs.DeviceFile
|
||||
import org.oxycblt.musikr.fs.device.DeviceFile
|
||||
import org.oxycblt.musikr.metadata.Properties
|
||||
import org.oxycblt.musikr.pipeline.RawSong
|
||||
import org.oxycblt.musikr.tag.Date
|
||||
|
@ -41,7 +42,7 @@ import org.oxycblt.musikr.tag.parse.ParsedTags
|
|||
import org.oxycblt.musikr.util.correctWhitespace
|
||||
import org.oxycblt.musikr.util.splitEscaped
|
||||
|
||||
@Database(entities = [CachedSong::class], version = 60, exportSchema = false)
|
||||
@Database(entities = [CachedSong::class], version = 61, exportSchema = false)
|
||||
internal abstract class CacheDatabase : RoomDatabase() {
|
||||
abstract fun visibleDao(): VisibleCacheDao
|
||||
|
||||
|
@ -118,13 +119,13 @@ internal data class CachedSong(
|
|||
val replayGainAlbumAdjustment: Float?,
|
||||
val coverId: String?,
|
||||
) {
|
||||
suspend fun intoRawSong(file: DeviceFile, covers: Covers): RawSong? {
|
||||
suspend fun intoRawSong(file: DeviceFile, covers: Covers<out Cover>): RawSong? {
|
||||
val cover =
|
||||
when (val result = coverId?.let { covers.obtain(it) }) {
|
||||
// We found the cover.
|
||||
is ObtainResult.Hit -> result.cover
|
||||
is CoverResult.Hit<out Cover> -> result.cover
|
||||
// We actually didn't find the cover, can't safely convert.
|
||||
is ObtainResult.Miss -> return null
|
||||
is CoverResult.Miss<out Cover> -> return null
|
||||
// No cover in the first place, can ignore.
|
||||
null -> null
|
||||
}
|
||||
|
|
|
@ -19,8 +19,9 @@
|
|||
package org.oxycblt.musikr.cache
|
||||
|
||||
import android.content.Context
|
||||
import org.oxycblt.musikr.cover.Cover
|
||||
import org.oxycblt.musikr.cover.Covers
|
||||
import org.oxycblt.musikr.fs.DeviceFile
|
||||
import org.oxycblt.musikr.fs.device.DeviceFile
|
||||
import org.oxycblt.musikr.pipeline.RawSong
|
||||
|
||||
interface StoredCache {
|
||||
|
@ -53,7 +54,7 @@ private abstract class BaseStoredCache(protected val writeDao: CacheWriteDao) :
|
|||
|
||||
private class VisibleStoredCache(private val visibleDao: VisibleCacheDao, writeDao: CacheWriteDao) :
|
||||
BaseStoredCache(writeDao) {
|
||||
override suspend fun read(file: DeviceFile, covers: Covers): CacheResult {
|
||||
override suspend fun read(file: DeviceFile, covers: Covers<out Cover>): CacheResult {
|
||||
val song = visibleDao.selectSong(file.uri.toString()) ?: return CacheResult.Miss(file, null)
|
||||
if (song.modifiedMs != file.modifiedMs) {
|
||||
// We *found* this file earlier, but it's out of date.
|
||||
|
@ -77,7 +78,7 @@ private class InvisibleStoredCache(
|
|||
private val invisibleCacheDao: InvisibleCacheDao,
|
||||
writeDao: CacheWriteDao
|
||||
) : BaseStoredCache(writeDao) {
|
||||
override suspend fun read(file: DeviceFile, covers: Covers) =
|
||||
override suspend fun read(file: DeviceFile, covers: Covers<out Cover>) =
|
||||
CacheResult.Miss(file, invisibleCacheDao.selectAddedMs(file.uri.toString()))
|
||||
|
||||
class Factory(private val cacheDatabase: CacheDatabase) : Cache.Factory() {
|
||||
|
|
|
@ -29,11 +29,13 @@ abstract class CoverFormat {
|
|||
|
||||
companion object {
|
||||
fun jpeg(params: CoverParams): CoverFormat =
|
||||
CoverFormatImpl("jpg", params, Bitmap.CompressFormat.JPEG)
|
||||
CompressingCoverFormat("jpg", params, Bitmap.CompressFormat.JPEG)
|
||||
|
||||
fun asIs(): CoverFormat = AsIsCoverFormat()
|
||||
}
|
||||
}
|
||||
|
||||
private class CoverFormatImpl(
|
||||
private class CompressingCoverFormat(
|
||||
override val extension: String,
|
||||
private val params: CoverParams,
|
||||
private val format: Bitmap.CompressFormat,
|
||||
|
@ -63,3 +65,16 @@ private class CoverFormatImpl(
|
|||
return inSampleSize
|
||||
}
|
||||
}
|
||||
|
||||
private class AsIsCoverFormat : CoverFormat() {
|
||||
override val extension: String = "bin"
|
||||
|
||||
override fun transcodeInto(data: ByteArray, output: OutputStream): Boolean {
|
||||
return try {
|
||||
output.write(data)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,27 +19,79 @@
|
|||
package org.oxycblt.musikr.cover
|
||||
|
||||
import java.io.InputStream
|
||||
import org.oxycblt.musikr.fs.device.DeviceFile
|
||||
import org.oxycblt.musikr.metadata.Metadata
|
||||
|
||||
interface Covers {
|
||||
suspend fun obtain(id: String): ObtainResult<out Cover>
|
||||
interface Covers<T : Cover> {
|
||||
suspend fun obtain(id: String): CoverResult<T>
|
||||
|
||||
companion object {
|
||||
fun <R : Cover, T : R> chain(vararg many: Covers<out T>): Covers<R> =
|
||||
object : Covers<R> {
|
||||
override suspend fun obtain(id: String): CoverResult<R> {
|
||||
for (cover in many) {
|
||||
val result = cover.obtain(id)
|
||||
if (result is CoverResult.Hit) {
|
||||
return CoverResult.Hit(result.cover)
|
||||
}
|
||||
}
|
||||
return CoverResult.Miss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface MutableCovers : Covers {
|
||||
suspend fun write(data: ByteArray): Cover
|
||||
interface MutableCovers<T : Cover> : Covers<T> {
|
||||
suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult<T>
|
||||
|
||||
suspend fun cleanup(excluding: Collection<Cover>)
|
||||
|
||||
companion object {
|
||||
fun <R : Cover, T : R> chain(vararg many: MutableCovers<out T>): MutableCovers<R> =
|
||||
object : MutableCovers<R> {
|
||||
override suspend fun obtain(id: String): CoverResult<R> {
|
||||
for (cover in many) {
|
||||
val result = cover.obtain(id)
|
||||
if (result is CoverResult.Hit) {
|
||||
return CoverResult.Hit(result.cover)
|
||||
}
|
||||
}
|
||||
return CoverResult.Miss()
|
||||
}
|
||||
|
||||
override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult<R> {
|
||||
for (cover in many) {
|
||||
val result = cover.create(file, metadata)
|
||||
if (result is CoverResult.Hit) {
|
||||
return CoverResult.Hit(result.cover)
|
||||
}
|
||||
}
|
||||
return CoverResult.Miss()
|
||||
}
|
||||
|
||||
override suspend fun cleanup(excluding: Collection<Cover>) {
|
||||
for (cover in many) {
|
||||
cover.cleanup(excluding)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface ObtainResult<T : Cover> {
|
||||
data class Hit<T : Cover>(val cover: T) : ObtainResult<T>
|
||||
sealed interface CoverResult<T : Cover> {
|
||||
data class Hit<T : Cover>(val cover: T) : CoverResult<T>
|
||||
|
||||
class Miss<T : Cover> : ObtainResult<T>
|
||||
class Miss<T : Cover> : CoverResult<T>
|
||||
}
|
||||
|
||||
interface Cover {
|
||||
val id: String
|
||||
|
||||
suspend fun open(): InputStream?
|
||||
|
||||
override fun equals(other: Any?): Boolean
|
||||
|
||||
override fun hashCode(): Int
|
||||
}
|
||||
|
||||
class CoverCollection private constructor(val covers: List<Cover>) {
|
||||
|
|
|
@ -21,15 +21,17 @@ package org.oxycblt.musikr.cover
|
|||
import android.os.ParcelFileDescriptor
|
||||
import org.oxycblt.musikr.fs.app.AppFile
|
||||
import org.oxycblt.musikr.fs.app.AppFiles
|
||||
import org.oxycblt.musikr.fs.device.DeviceFile
|
||||
import org.oxycblt.musikr.metadata.Metadata
|
||||
|
||||
open class FileCovers(private val appFiles: AppFiles, private val coverFormat: CoverFormat) :
|
||||
Covers {
|
||||
override suspend fun obtain(id: String): ObtainResult<FileCover> {
|
||||
Covers<FileCover> {
|
||||
override suspend fun obtain(id: String): CoverResult<FileCover> {
|
||||
val file = appFiles.find(getFileName(id))
|
||||
return if (file != null) {
|
||||
ObtainResult.Hit(FileCoverImpl(id, file))
|
||||
CoverResult.Hit(FileCoverImpl(id, file))
|
||||
} else {
|
||||
ObtainResult.Miss()
|
||||
CoverResult.Miss()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -40,11 +42,12 @@ class MutableFileCovers(
|
|||
private val appFiles: AppFiles,
|
||||
private val coverFormat: CoverFormat,
|
||||
private val coverIdentifier: CoverIdentifier
|
||||
) : FileCovers(appFiles, coverFormat), MutableCovers {
|
||||
override suspend fun write(data: ByteArray): FileCover {
|
||||
) : FileCovers(appFiles, coverFormat), MutableCovers<FileCover> {
|
||||
override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult<FileCover> {
|
||||
val data = metadata.cover ?: return CoverResult.Miss()
|
||||
val id = coverIdentifier.identify(data)
|
||||
val file = appFiles.write(getFileName(id)) { coverFormat.transcodeInto(data, it) }
|
||||
return FileCoverImpl(id, file)
|
||||
val coverFile = appFiles.write(getFileName(id)) { coverFormat.transcodeInto(data, it) }
|
||||
return CoverResult.Hit(FileCoverImpl(id, coverFile))
|
||||
}
|
||||
|
||||
override suspend fun cleanup(excluding: Collection<Cover>) {
|
||||
|
|
131
musikr/src/main/java/org/oxycblt/musikr/cover/FolderCovers.kt
Normal file
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Auxio Project
|
||||
* FolderCovers.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.musikr.cover
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.ParcelFileDescriptor
|
||||
import java.io.InputStream
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.oxycblt.musikr.fs.device.DeviceDirectory
|
||||
import org.oxycblt.musikr.fs.device.DeviceFile
|
||||
import org.oxycblt.musikr.metadata.Metadata
|
||||
|
||||
open class FolderCovers(private val context: Context) : Covers<FolderCover> {
|
||||
override suspend fun obtain(id: String): CoverResult<FolderCover> {
|
||||
// Parse the ID to get the directory URI
|
||||
if (!id.startsWith("folder:")) {
|
||||
return CoverResult.Miss()
|
||||
}
|
||||
|
||||
// TODO: Check if the dir actually exists still to avoid stale uris
|
||||
val directoryUri = id.substring("folder:".length)
|
||||
val uri = Uri.parse(directoryUri)
|
||||
|
||||
// Check if the URI is still valid
|
||||
val exists =
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
context.contentResolver.openFileDescriptor(uri, "r")?.close()
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
return if (exists) {
|
||||
CoverResult.Hit(FolderCoverImpl(context, uri))
|
||||
} else {
|
||||
CoverResult.Miss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MutableFolderCovers(private val context: Context) :
|
||||
FolderCovers(context), MutableCovers<FolderCover> {
|
||||
override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult<FolderCover> {
|
||||
val parent = file.parent.await()
|
||||
val coverFile = findCoverInDirectory(parent) ?: return CoverResult.Miss()
|
||||
return CoverResult.Hit(FolderCoverImpl(context, coverFile.uri))
|
||||
}
|
||||
|
||||
override suspend fun cleanup(excluding: Collection<Cover>) {
|
||||
// No cleanup needed for folder covers as they are external files
|
||||
// that should not be managed by the app
|
||||
}
|
||||
|
||||
private suspend fun findCoverInDirectory(directory: DeviceDirectory): DeviceFile? {
|
||||
return directory.children
|
||||
.mapNotNull { node -> if (node is DeviceFile && isCoverArtFile(node)) node else null }
|
||||
.firstOrNull()
|
||||
}
|
||||
|
||||
private fun isCoverArtFile(file: DeviceFile): Boolean {
|
||||
val filename = requireNotNull(file.path.name).lowercase()
|
||||
val mimeType = file.mimeType.lowercase()
|
||||
|
||||
// Check if the file is an image
|
||||
if (!mimeType.startsWith("image/")) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Common cover art filenames
|
||||
val coverNames =
|
||||
listOf(
|
||||
"cover",
|
||||
"folder",
|
||||
"album",
|
||||
"albumart",
|
||||
"front",
|
||||
"artwork",
|
||||
"art",
|
||||
"folder",
|
||||
"coverart")
|
||||
|
||||
// Check if the filename matches any common cover art names
|
||||
// Also check for case variations (e.g., Cover.jpg, COVER.JPG)
|
||||
val filenameWithoutExt = filename.substringBeforeLast(".")
|
||||
val extension = filename.substringAfterLast(".", "")
|
||||
|
||||
return coverNames.any { coverName ->
|
||||
filenameWithoutExt.equals(coverName, ignoreCase = true) &&
|
||||
(extension.equals("jpg", ignoreCase = true) ||
|
||||
extension.equals("jpeg", ignoreCase = true) ||
|
||||
extension.equals("png", ignoreCase = true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface FolderCover : FileCover
|
||||
|
||||
private data class FolderCoverImpl(
|
||||
private val context: Context,
|
||||
private val uri: Uri,
|
||||
) : FolderCover {
|
||||
override val id = "folder:$uri"
|
||||
|
||||
override suspend fun fd(): ParcelFileDescriptor? =
|
||||
withContext(Dispatchers.IO) { context.contentResolver.openFileDescriptor(uri, "r") }
|
||||
|
||||
override suspend fun open(): InputStream? =
|
||||
withContext(Dispatchers.IO) { context.contentResolver.openInputStream(uri) }
|
||||
}
|
|
@ -16,14 +16,29 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.musikr.fs
|
||||
package org.oxycblt.musikr.fs.device
|
||||
|
||||
import android.net.Uri
|
||||
import kotlinx.coroutines.Deferred
|
||||
import org.oxycblt.musikr.fs.Path
|
||||
|
||||
internal data class DeviceFile(
|
||||
val uri: Uri,
|
||||
sealed interface DeviceNode {
|
||||
val uri: Uri
|
||||
val path: Path
|
||||
}
|
||||
|
||||
data class DeviceDirectory(
|
||||
override val uri: Uri,
|
||||
override val path: Path,
|
||||
val parent: Deferred<DeviceDirectory>?,
|
||||
val children: List<DeviceNode>
|
||||
) : DeviceNode
|
||||
|
||||
data class DeviceFile(
|
||||
override val uri: Uri,
|
||||
override val path: Path,
|
||||
val modifiedMs: Long,
|
||||
val mimeType: String,
|
||||
val path: Path,
|
||||
val size: Long,
|
||||
val modifiedMs: Long
|
||||
)
|
||||
val parent: Deferred<DeviceDirectory>
|
||||
) : DeviceNode
|
|
@ -22,6 +22,8 @@ import android.content.ContentResolver
|
|||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.provider.DocumentsContract
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
|
@ -29,7 +31,6 @@ import kotlinx.coroutines.flow.emitAll
|
|||
import kotlinx.coroutines.flow.flatMapMerge
|
||||
import kotlinx.coroutines.flow.flattenMerge
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import org.oxycblt.musikr.fs.DeviceFile
|
||||
import org.oxycblt.musikr.fs.MusicLocation
|
||||
import org.oxycblt.musikr.fs.Path
|
||||
|
||||
|
@ -37,66 +38,82 @@ internal interface DeviceFiles {
|
|||
fun explore(locations: Flow<MusicLocation>): Flow<DeviceFile>
|
||||
|
||||
companion object {
|
||||
fun from(context: Context): DeviceFiles = DeviceFilesImpl(context.contentResolverSafe)
|
||||
fun from(context: Context, ignoreHidden: Boolean): DeviceFiles =
|
||||
DeviceFilesImpl(context.contentResolverSafe, ignoreHidden)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private class DeviceFilesImpl(private val contentResolver: ContentResolver) : DeviceFiles {
|
||||
private class DeviceFilesImpl(
|
||||
private val contentResolver: ContentResolver,
|
||||
private val ignoreHidden: Boolean
|
||||
) : DeviceFiles {
|
||||
override fun explore(locations: Flow<MusicLocation>): Flow<DeviceFile> =
|
||||
locations.flatMapMerge { location ->
|
||||
exploreImpl(
|
||||
contentResolver,
|
||||
// Set up the children flow for the root directory
|
||||
exploreDirectoryImpl(
|
||||
location.uri,
|
||||
DocumentsContract.getTreeDocumentId(location.uri),
|
||||
location.path)
|
||||
location.path,
|
||||
null)
|
||||
}
|
||||
|
||||
private fun exploreImpl(
|
||||
contentResolver: ContentResolver,
|
||||
private fun exploreDirectoryImpl(
|
||||
rootUri: Uri,
|
||||
treeDocumentId: String,
|
||||
relativePath: Path
|
||||
relativePath: Path,
|
||||
parent: Deferred<DeviceDirectory>?
|
||||
): Flow<DeviceFile> = flow {
|
||||
contentResolver.useQuery(
|
||||
DocumentsContract.buildChildDocumentsUriUsingTree(rootUri, treeDocumentId),
|
||||
PROJECTION) { cursor ->
|
||||
val childUriIndex =
|
||||
cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID)
|
||||
val displayNameIndex =
|
||||
cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DISPLAY_NAME)
|
||||
val mimeTypeIndex =
|
||||
cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_MIME_TYPE)
|
||||
val sizeIndex = cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_SIZE)
|
||||
val lastModifiedIndex =
|
||||
cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_LAST_MODIFIED)
|
||||
val recursive = mutableListOf<Flow<DeviceFile>>()
|
||||
while (cursor.moveToNext()) {
|
||||
val childId = cursor.getString(childUriIndex)
|
||||
val displayName = cursor.getString(displayNameIndex)
|
||||
val newPath = relativePath.file(displayName)
|
||||
val mimeType = cursor.getString(mimeTypeIndex)
|
||||
if (mimeType == DocumentsContract.Document.MIME_TYPE_DIR) {
|
||||
// This does NOT block the current coroutine. Instead, we will
|
||||
// evaluate this flow in parallel later to maximize throughput.
|
||||
recursive.add(exploreImpl(contentResolver, rootUri, childId, newPath))
|
||||
} else if (mimeType.startsWith("audio/") && mimeType != "audio/x-mpegurl") {
|
||||
// Immediately emit all files given that it's just an O(1) op.
|
||||
// This also just makes sure the outer flow has a reason to exist
|
||||
// rather than just being a glorified async.
|
||||
val lastModified = cursor.getLong(lastModifiedIndex)
|
||||
val size = cursor.getLong(sizeIndex)
|
||||
emit(
|
||||
DeviceFile(
|
||||
DocumentsContract.buildDocumentUriUsingTree(rootUri, childId),
|
||||
mimeType,
|
||||
newPath,
|
||||
size,
|
||||
lastModified))
|
||||
}
|
||||
// Make a kotlin future
|
||||
val uri = DocumentsContract.buildChildDocumentsUriUsingTree(rootUri, treeDocumentId)
|
||||
val directoryDeferred = CompletableDeferred<DeviceDirectory>()
|
||||
val recursive = mutableListOf<Flow<DeviceFile>>()
|
||||
val children = mutableListOf<DeviceNode>()
|
||||
contentResolver.useQuery(uri, PROJECTION) { cursor ->
|
||||
val childUriIndex =
|
||||
cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID)
|
||||
val displayNameIndex =
|
||||
cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DISPLAY_NAME)
|
||||
val mimeTypeIndex =
|
||||
cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_MIME_TYPE)
|
||||
val sizeIndex = cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_SIZE)
|
||||
val lastModifiedIndex =
|
||||
cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_LAST_MODIFIED)
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
val childId = cursor.getString(childUriIndex)
|
||||
val displayName = cursor.getString(displayNameIndex)
|
||||
|
||||
// Skip hidden files/directories if ignoreHidden is true
|
||||
if (ignoreHidden && displayName.startsWith(".")) {
|
||||
continue
|
||||
}
|
||||
|
||||
val newPath = relativePath.file(displayName)
|
||||
val mimeType = cursor.getString(mimeTypeIndex)
|
||||
val lastModified = cursor.getLong(lastModifiedIndex)
|
||||
|
||||
if (mimeType == DocumentsContract.Document.MIME_TYPE_DIR) {
|
||||
recursive.add(
|
||||
exploreDirectoryImpl(rootUri, childId, newPath, directoryDeferred))
|
||||
} else {
|
||||
val size = cursor.getLong(sizeIndex)
|
||||
val childUri = DocumentsContract.buildDocumentUriUsingTree(rootUri, childId)
|
||||
val file =
|
||||
DeviceFile(
|
||||
uri = childUri,
|
||||
mimeType = mimeType,
|
||||
path = newPath,
|
||||
size = size,
|
||||
modifiedMs = lastModified,
|
||||
parent = directoryDeferred)
|
||||
children.add(file)
|
||||
emit(file)
|
||||
}
|
||||
emitAll(recursive.asFlow().flattenMerge())
|
||||
}
|
||||
}
|
||||
directoryDeferred.complete(DeviceDirectory(uri, relativePath, parent, children))
|
||||
emitAll(recursive.asFlow().flattenMerge())
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
|
|
@ -144,6 +144,8 @@ private class MusicGraphBuilderImpl : MusicGraph.Builder {
|
|||
it.pointerMap[v363Pointer]?.forEach { index -> it.songVertices[index] = vertex }
|
||||
val v400Pointer = SongPointer.UID(entry.value.preSong.v400Uid)
|
||||
it.pointerMap[v400Pointer]?.forEach { index -> it.songVertices[index] = vertex }
|
||||
val v401Pointer = SongPointer.UID(entry.value.preSong.v401Uid)
|
||||
it.pointerMap[v401Pointer]?.forEach { index -> it.songVertices[index] = vertex }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
package org.oxycblt.musikr.metadata
|
||||
|
||||
internal data class Metadata(
|
||||
data class Metadata(
|
||||
val id3v2: Map<String, List<String>>,
|
||||
val xiph: Map<String, List<String>>,
|
||||
val mp4: Map<String, List<String>>,
|
||||
|
@ -53,7 +53,7 @@ internal data class Metadata(
|
|||
}
|
||||
}
|
||||
|
||||
internal data class Properties(
|
||||
data class Properties(
|
||||
val mimeType: String,
|
||||
val durationMs: Long,
|
||||
val bitrateKbps: Int,
|
||||
|
|
|
@ -22,7 +22,7 @@ import android.os.ParcelFileDescriptor
|
|||
import java.io.FileInputStream
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.oxycblt.musikr.fs.DeviceFile
|
||||
import org.oxycblt.musikr.fs.device.DeviceFile
|
||||
|
||||
internal interface MetadataExtractor {
|
||||
suspend fun extract(deviceFile: DeviceFile, fd: ParcelFileDescriptor): Metadata?
|
||||
|
|
|
@ -21,7 +21,7 @@ package org.oxycblt.musikr.metadata
|
|||
import android.util.Log
|
||||
import java.io.FileInputStream
|
||||
import java.nio.ByteBuffer
|
||||
import org.oxycblt.musikr.fs.DeviceFile
|
||||
import org.oxycblt.musikr.fs.device.DeviceFile
|
||||
|
||||
internal class NativeInputStream(private val deviceFile: DeviceFile, fis: FileInputStream) {
|
||||
private val channel = fis.channel
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
package org.oxycblt.musikr.metadata
|
||||
|
||||
import java.io.FileInputStream
|
||||
import org.oxycblt.musikr.fs.DeviceFile
|
||||
import org.oxycblt.musikr.fs.device.DeviceFile
|
||||
|
||||
internal object TagLibJNI {
|
||||
init {
|
||||
|
|
|
@ -38,6 +38,7 @@ internal data class LibraryImpl(
|
|||
) : MutableLibrary {
|
||||
private val songUidMap = songs.associateBy { it.uid }
|
||||
private val v400SongUidMap = songs.associateBy { it.v400Uid }
|
||||
private val v401SongUidMap = songs.associateBy { it.v401Uid }
|
||||
private val albumUidMap = albums.associateBy { it.uid }
|
||||
private val artistUidMap = artists.associateBy { it.uid }
|
||||
private val genreUidMap = genres.associateBy { it.uid }
|
||||
|
@ -46,7 +47,8 @@ internal data class LibraryImpl(
|
|||
override fun empty() = songs.isEmpty()
|
||||
|
||||
// Compat hack. See TagInterpreter for why this needs to be done
|
||||
override fun findSong(uid: Music.UID) = songUidMap[uid] ?: v400SongUidMap[uid]
|
||||
override fun findSong(uid: Music.UID) =
|
||||
songUidMap[uid] ?: v400SongUidMap[uid] ?: v401SongUidMap[uid]
|
||||
|
||||
override fun findSongByPath(path: Path) = songs.find { it.path == path }
|
||||
|
||||
|
|
|
@ -46,6 +46,8 @@ internal class SongImpl(private val handle: SongCore) : Song {
|
|||
|
||||
val v400Uid = preSong.v400Uid
|
||||
|
||||
val v401Uid = preSong.v401Uid
|
||||
|
||||
override val name = preSong.name
|
||||
override val track = preSong.track
|
||||
override val disc = preSong.disc
|
||||
|
|
|
@ -24,14 +24,15 @@ import kotlinx.coroutines.flow.Flow
|
|||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.buffer
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import org.oxycblt.musikr.Interpretation
|
||||
import org.oxycblt.musikr.Storage
|
||||
import org.oxycblt.musikr.fs.DeviceFile
|
||||
import org.oxycblt.musikr.fs.MusicLocation
|
||||
import org.oxycblt.musikr.fs.device.DeviceFile
|
||||
import org.oxycblt.musikr.fs.device.DeviceFiles
|
||||
import org.oxycblt.musikr.playlist.PlaylistFile
|
||||
import org.oxycblt.musikr.playlist.db.StoredPlaylists
|
||||
|
@ -41,8 +42,9 @@ internal interface ExploreStep {
|
|||
fun explore(locations: List<MusicLocation>): Flow<ExploreNode>
|
||||
|
||||
companion object {
|
||||
fun from(context: Context, storage: Storage): ExploreStep =
|
||||
ExploreStepImpl(DeviceFiles.from(context), storage.storedPlaylists)
|
||||
fun from(context: Context, storage: Storage, interpretation: Interpretation): ExploreStep =
|
||||
ExploreStepImpl(
|
||||
DeviceFiles.from(context, interpretation.ignoreHidden), storage.storedPlaylists)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,13 +56,8 @@ private class ExploreStepImpl(
|
|||
val audios =
|
||||
deviceFiles
|
||||
.explore(locations.asFlow())
|
||||
.mapNotNull {
|
||||
when {
|
||||
it.mimeType == M3U.MIME_TYPE -> null
|
||||
it.mimeType.startsWith("audio/") -> ExploreNode.Audio(it)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
.filter { it.mimeType.startsWith("audio/") && it.mimeType != M3U.MIME_TYPE }
|
||||
.map { ExploreNode.Audio(it) }
|
||||
.flowOn(Dispatchers.IO)
|
||||
.buffer()
|
||||
val playlists =
|
||||
|
|
|
@ -35,8 +35,9 @@ import org.oxycblt.musikr.Storage
|
|||
import org.oxycblt.musikr.cache.Cache
|
||||
import org.oxycblt.musikr.cache.CacheResult
|
||||
import org.oxycblt.musikr.cover.Cover
|
||||
import org.oxycblt.musikr.cover.CoverResult
|
||||
import org.oxycblt.musikr.cover.MutableCovers
|
||||
import org.oxycblt.musikr.fs.DeviceFile
|
||||
import org.oxycblt.musikr.fs.device.DeviceFile
|
||||
import org.oxycblt.musikr.metadata.MetadataExtractor
|
||||
import org.oxycblt.musikr.metadata.Properties
|
||||
import org.oxycblt.musikr.playlist.PlaylistFile
|
||||
|
@ -62,7 +63,7 @@ private class ExtractStepImpl(
|
|||
private val metadataExtractor: MetadataExtractor,
|
||||
private val tagParser: TagParser,
|
||||
private val cacheFactory: Cache.Factory,
|
||||
private val storedCovers: MutableCovers
|
||||
private val covers: MutableCovers<out Cover>
|
||||
) : ExtractStep {
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override fun extract(nodes: Flow<ExploreNode>): Flow<ExtractedMusic> {
|
||||
|
@ -78,17 +79,20 @@ private class ExtractStepImpl(
|
|||
val audioNodes = filterFlow.right
|
||||
val playlistNodes = filterFlow.left.map { ExtractedMusic.Valid.Playlist(it) }
|
||||
|
||||
// First distribute audio nodes for parallel cache reading
|
||||
val readDistributedFlow = audioNodes.distribute(8)
|
||||
val cacheResults =
|
||||
readDistributedFlow.flows
|
||||
.map { flow ->
|
||||
flow
|
||||
.map { wrap(it) { file -> cache.read(file, storedCovers) } }
|
||||
.map { wrap(it) { file -> cache.read(file, covers) } }
|
||||
.flowOn(Dispatchers.IO)
|
||||
.buffer(Channel.UNLIMITED)
|
||||
}
|
||||
.flattenMerge()
|
||||
.buffer(Channel.UNLIMITED)
|
||||
|
||||
// Divert cache hits and misses
|
||||
val cacheFlow =
|
||||
cacheResults.divert {
|
||||
when (it) {
|
||||
|
@ -96,89 +100,84 @@ private class ExtractStepImpl(
|
|||
is CacheResult.Miss -> Divert.Right(it.file)
|
||||
}
|
||||
}
|
||||
|
||||
// Cache hits can be directly converted to valid songs
|
||||
val cachedSongs = cacheFlow.left.map { ExtractedMusic.Valid.Song(it) }
|
||||
val uncachedSongs = cacheFlow.right
|
||||
|
||||
val fds =
|
||||
uncachedSongs
|
||||
.mapNotNull {
|
||||
wrap(it) { file ->
|
||||
withContext(Dispatchers.IO) {
|
||||
context.contentResolver.openFileDescriptor(file.uri, "r")?.let { fd ->
|
||||
FileWith(file, fd)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.flowOn(Dispatchers.IO)
|
||||
.buffer(Channel.UNLIMITED)
|
||||
// Process uncached files in parallel
|
||||
val uncachedFiles = cacheFlow.right
|
||||
val processingDistributedFlow = uncachedFiles.distribute(8)
|
||||
|
||||
val metadata =
|
||||
fds.mapNotNull { fileWith ->
|
||||
wrap(fileWith.file) { _ ->
|
||||
metadataExtractor
|
||||
.extract(fileWith.file, fileWith.with)
|
||||
.let { FileWith(fileWith.file, it) }
|
||||
.also { withContext(Dispatchers.IO) { fileWith.with.close() } }
|
||||
}
|
||||
}
|
||||
.flowOn(Dispatchers.IO)
|
||||
// Covers are pretty big, so cap the amount of parsed metadata in-memory to at most
|
||||
// 8 to minimize GCs.
|
||||
.buffer(8)
|
||||
|
||||
val extractedSongs =
|
||||
metadata
|
||||
.map { fileWith ->
|
||||
if (fileWith.with != null) {
|
||||
val tags = tagParser.parse(fileWith.with)
|
||||
val cover = fileWith.with.cover?.let { storedCovers.write(it) }
|
||||
RawSong(fileWith.file, fileWith.with.properties, tags, cover, addingMs)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
.flowOn(Dispatchers.IO)
|
||||
.buffer(Channel.UNLIMITED)
|
||||
|
||||
val extractedFilter =
|
||||
extractedSongs.divert {
|
||||
if (it != null) Divert.Left(it) else Divert.Right(ExtractedMusic.Invalid)
|
||||
}
|
||||
|
||||
val write = extractedFilter.left
|
||||
val invalid = extractedFilter.right
|
||||
|
||||
val writeDistributedFlow = write.distribute(8)
|
||||
val writtenSongs =
|
||||
writeDistributedFlow.flows
|
||||
// Process each uncached file in parallel flows
|
||||
val processedSongs =
|
||||
processingDistributedFlow.flows
|
||||
.map { flow ->
|
||||
flow
|
||||
.map {
|
||||
wrap(it, cache::write)
|
||||
ExtractedMusic.Valid.Song(it)
|
||||
.mapNotNull { file ->
|
||||
wrap(file) { f ->
|
||||
withContext(Dispatchers.IO) {
|
||||
context.contentResolver.openFileDescriptor(f.uri, "r")
|
||||
}
|
||||
?.use {
|
||||
val extractedMetadata = metadataExtractor.extract(file, it)
|
||||
|
||||
if (extractedMetadata != null) {
|
||||
val tags = tagParser.parse(extractedMetadata)
|
||||
val cover =
|
||||
when (val result =
|
||||
covers.create(f, extractedMetadata)) {
|
||||
is CoverResult.Hit -> result.cover
|
||||
else -> null
|
||||
}
|
||||
val rawSong =
|
||||
RawSong(
|
||||
f,
|
||||
extractedMetadata.properties,
|
||||
tags,
|
||||
cover,
|
||||
addingMs)
|
||||
cache.write(rawSong)
|
||||
|
||||
ExtractedMusic.Valid.Song(rawSong)
|
||||
} else {
|
||||
ExtractedMusic.Invalid
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.flowOn(Dispatchers.IO)
|
||||
.buffer(Channel.UNLIMITED)
|
||||
}
|
||||
.flattenMerge()
|
||||
.buffer(Channel.UNLIMITED)
|
||||
|
||||
// Separate valid processed songs from invalid ones
|
||||
val processedFlow =
|
||||
processedSongs.divert {
|
||||
when (it) {
|
||||
is ExtractedMusic.Valid.Song -> Divert.Left(it)
|
||||
is ExtractedMusic.Invalid -> Divert.Right(it)
|
||||
else -> Divert.Right(ExtractedMusic.Invalid)
|
||||
}
|
||||
}
|
||||
|
||||
val processedValidSongs = processedFlow.left
|
||||
val invalidSongs = processedFlow.right
|
||||
|
||||
val merged =
|
||||
merge(
|
||||
filterFlow.manager,
|
||||
readDistributedFlow.manager,
|
||||
cacheFlow.manager,
|
||||
processingDistributedFlow.manager,
|
||||
processedFlow.manager,
|
||||
cachedSongs,
|
||||
extractedFilter.manager,
|
||||
writeDistributedFlow.manager,
|
||||
writtenSongs,
|
||||
invalid,
|
||||
processedValidSongs,
|
||||
invalidSongs,
|
||||
playlistNodes)
|
||||
|
||||
return merged.onCompletion { cache.finalize() }
|
||||
}
|
||||
|
||||
private data class FileWith<T>(val file: DeviceFile, val with: T)
|
||||
}
|
||||
|
||||
internal data class RawSong(
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
package org.oxycblt.musikr.pipeline
|
||||
|
||||
import org.oxycblt.musikr.fs.DeviceFile
|
||||
import org.oxycblt.musikr.fs.device.DeviceFile
|
||||
import org.oxycblt.musikr.playlist.PlaylistFile
|
||||
import org.oxycblt.musikr.playlist.interpret.PrePlaylist
|
||||
import org.oxycblt.musikr.tag.interpret.PreSong
|
||||
|
|
|
@ -33,6 +33,7 @@ import org.oxycblt.musikr.tag.ReplayGainAdjustment
|
|||
internal data class PreSong(
|
||||
val v363Uid: Music.UID,
|
||||
val v400Uid: Music.UID,
|
||||
val v401Uid: Music.UID,
|
||||
val musicBrainzId: UUID?,
|
||||
val name: Name.Known,
|
||||
val rawName: String,
|
||||
|
|
|
@ -20,8 +20,8 @@ package org.oxycblt.musikr.tag.interpret
|
|||
|
||||
import org.oxycblt.musikr.Interpretation
|
||||
import org.oxycblt.musikr.Music
|
||||
import org.oxycblt.musikr.fs.DeviceFile
|
||||
import org.oxycblt.musikr.fs.Format
|
||||
import org.oxycblt.musikr.fs.device.DeviceFile
|
||||
import org.oxycblt.musikr.pipeline.RawSong
|
||||
import org.oxycblt.musikr.tag.Disc
|
||||
import org.oxycblt.musikr.tag.Name
|
||||
|
@ -67,13 +67,15 @@ private class TagInterpreterImpl(private val interpretation: Interpretation) : T
|
|||
val songNameOrFile = song.tags.name ?: requireNotNull(song.file.path.name)
|
||||
val songNameOrFileWithoutExt =
|
||||
song.tags.name ?: requireNotNull(song.file.path.name).split('.').first()
|
||||
val songNameOrFileWithoutExtCorrect =
|
||||
song.tags.name ?: requireNotNull(song.file.path.name).substringBeforeLast(".")
|
||||
val albumNameOrDir = song.tags.albumName ?: song.file.path.directory.name
|
||||
|
||||
val musicBrainzId = song.tags.musicBrainzId?.toUuidOrNull()
|
||||
val v363uid =
|
||||
musicBrainzId?.let { Music.UID.musicBrainz(Music.UID.Item.SONG, it) }
|
||||
?: Music.UID.auxio(Music.UID.Item.SONG) {
|
||||
update(songNameOrFileWithoutExt)
|
||||
update(songNameOrFileWithoutExtCorrect)
|
||||
update(albumNameOrDir)
|
||||
update(song.tags.date)
|
||||
|
||||
|
@ -103,9 +105,24 @@ private class TagInterpreterImpl(private val interpretation: Interpretation) : T
|
|||
update(albumArtistNames.ifEmpty { artistNames }.ifEmpty { listOf(null) })
|
||||
}
|
||||
|
||||
val v401uid =
|
||||
musicBrainzId?.let { Music.UID.musicBrainz(Music.UID.Item.SONG, it) }
|
||||
?: Music.UID.auxio(Music.UID.Item.SONG) {
|
||||
update(songNameOrFileWithoutExt)
|
||||
update(albumNameOrDir)
|
||||
update(song.tags.date)
|
||||
|
||||
update(song.tags.track)
|
||||
update(song.tags.disc)
|
||||
|
||||
update(song.tags.artistNames)
|
||||
update(song.tags.albumArtistNames)
|
||||
}
|
||||
|
||||
return PreSong(
|
||||
v363Uid = v363uid,
|
||||
v400Uid = v400uid,
|
||||
v401Uid = v401uid,
|
||||
uri = uri,
|
||||
path = song.file.path,
|
||||
size = song.file.size,
|
||||
|
@ -113,8 +130,8 @@ private class TagInterpreterImpl(private val interpretation: Interpretation) : T
|
|||
modifiedMs = song.file.modifiedMs,
|
||||
addedMs = song.addedMs,
|
||||
musicBrainzId = musicBrainzId,
|
||||
name = interpretation.naming.name(songNameOrFileWithoutExt, song.tags.sortName),
|
||||
rawName = songNameOrFileWithoutExt,
|
||||
name = interpretation.naming.name(songNameOrFileWithoutExtCorrect, song.tags.sortName),
|
||||
rawName = songNameOrFileWithoutExtCorrect,
|
||||
track = song.tags.track,
|
||||
disc = song.tags.disc?.let { Disc(it, song.tags.subtitle) },
|
||||
date = song.tags.date,
|
||||
|
|