Merge pull request #1024 from OxygenCobalt/dev

v4.0.2
This commit is contained in:
Alexander Capehart 2025-03-04 18:42:12 -07:00 committed by GitHub
commit 971c0e3a25
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
67 changed files with 1027 additions and 339 deletions

2
.gitignore vendored
View file

@ -14,3 +14,5 @@ captures/
*.iml
.cxx
.kotlin
.aider*
.env

View file

@ -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

View file

@ -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">

View file

@ -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

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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
}
}
}

View file

@ -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))
}

View file

@ -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)
}
}
}

View file

@ -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() }

View file

@ -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))
}

View file

@ -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)
}
}

View file

@ -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)

View file

@ -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()
}

View file

@ -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
}
}
}
}

View file

@ -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
)

View file

@ -266,4 +266,4 @@
<string name="lng_empty_genres">الفئات الخاصة بك ستضهر هنا.</string>
<string name="set_observing_desc">اعادة تحميل مكتبة الموسيقى عند حصول تغيير(يتطلب تنبيه ثابت)</string>
<string name="set_separators_warning">تحذير: استخدام هذا الاعداد قد ينتج عنه ان يتم تفسير بعض العلامات بشكل خاطئ مثل ان تحتوي على قيم متعددة. يمكن ان يتم حل هذا بتقديم الفواصل الغير مرغوبةبالشارحة الخلفية(\\).</string>
</resources>
</resources>

View file

@ -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 (&amp;)</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>

View file

@ -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 (&amp;)</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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View 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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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)

View file

@ -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)

View 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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 323 KiB

After

Width:  |  Height:  |  Size: 323 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 265 KiB

After

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 KiB

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 151 KiB

View file

@ -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)

View file

@ -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)

View file

@ -1 +1 @@
Un semplice, razionale lettore musicale
Un lettore musicale semplice e razionale

View file

@ -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).

View file

@ -6,8 +6,7 @@ Auxio — це локальний музичний плеєр із швидки
- Snappy UI, створений на основі останніх рекомендацій Material Design
- Переконливий UX, який надає перевагу простоті використання над крайніми випадками
- Настроювана поведінка
- Підтримка номерів дисків, кількох виконавців, типів випусків,
точні/оригінальні дати, теги сортування тощо
- Підтримка номерів дисків, кількох виконавців, типів випусків, точних/оригінальних дат, тегів сортування тощо
— Розширена система виконавців, яка об’єднує виконавців і виконавців альбомів
- Керування папками з підтримкою SD-карти
— Надійна функція списків відтворення
@ -22,4 +21,4 @@ Auxio — це локальний музичний плеєр із швидки
- Автовідтворення гарнітури
- Стильні віджети, які автоматично адаптуються до їх розміру
- Повністю приватний і офлайн
- Немає округлених обкладинок альбомів (за замовчуванням)
- Немає закруглених обкладинок альбомів (якщо ви їх хочете)

View file

@ -6,8 +6,7 @@ Auxio 是一款本地音乐播放器,它拥有快速、可靠的 UI/UX
- 源于最新 Material You 设计规范的灵动界面
- 优先考虑易用性的独到用户体验
- 可定制的播放器行为
- 支持唱片编号、多名艺术家、发布类型、精确/原始日期、
标签排序及其他更多功能
- 支持唱片编号、多名艺术家、发行类型、精确/原始日期、排序标签以及更多
- 统一“艺术家”和“专辑艺术家”的高级“艺术家”系统
- 文件夹管理功能可以感知到 SD 卡
- 可靠的播放列表功能
@ -22,4 +21,4 @@ Auxio 是一款本地音乐播放器,它拥有快速、可靠的 UI/UX
- 耳机连接时自动播放
- 按桌面尺寸自适应的风格化微件
- 完全离线且私密
- 没有圆角的专辑封面(默认设置
- 没有圆角的专辑封面(即使你想要

View file

@ -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;
}
}

View file

@ -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
)

View file

@ -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))
}

View file

@ -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)

View file

@ -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
}

View file

@ -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() {

View file

@ -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
}
}
}

View file

@ -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>) {

View file

@ -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>) {

View 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) }
}

View file

@ -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

View file

@ -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 {

View file

@ -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 }
}
}

View file

@ -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,

View file

@ -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?

View file

@ -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

View file

@ -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 {

View file

@ -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 }

View file

@ -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

View file

@ -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 =

View file

@ -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(

View file

@ -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

View file

@ -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,

View file

@ -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,