diff --git a/.gitignore b/.gitignore
index d461864a6..a6dda7e2f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,3 +14,5 @@ captures/
*.iml
.cxx
.kotlin
+.aider*
+.env
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4792b87bd..659f7d6c5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,23 @@
# Changelog
+## 4.0.2
+
+#### What's New
+- Added back in support for cover art from cover.png/cover.jpg
+- Added "As is" cover art setting
+- Option to include hidden files or not (off by default)
+
+#### What's Improved
+- Reduced elevation contrast in black theme
+
+#### What's Fixed
+- Fixed incorrect extension stripping on some files
+- Fixed various errors in new branding
+- Fixed MTE segfault from improper string handling
+
+#### What's Changed
+- Hidden files no longer loaded by default
+
## 4.0.1
#### What's Fixed
diff --git a/README.md b/README.md
index 07052404a..f74c258cf 100644
--- a/README.md
+++ b/README.md
@@ -2,8 +2,8 @@
Auxio
A simple, rational music player for android.
-
-
+
+
diff --git a/app/build.gradle b/app/build.gradle
index fdc1e0eab..d444f70c9 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -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
diff --git a/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt b/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt
index 1da5824b9..55907bb0d 100644
--- a/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt
+++ b/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt
@@ -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
}
diff --git a/app/src/main/java/org/oxycblt/auxio/image/CoverMode.kt b/app/src/main/java/org/oxycblt/auxio/image/CoverMode.kt
index cad4a2ab2..a0cf8fe83 100644
--- a/app/src/main/java/org/oxycblt/auxio/image/CoverMode.kt
+++ b/app/src/main/java/org/oxycblt/auxio/image/CoverMode.kt
@@ -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
}
}
diff --git a/app/src/main/java/org/oxycblt/auxio/image/CoverProvider.kt b/app/src/main/java/org/oxycblt/auxio/image/CoverProvider.kt
index 976fc64e4..5c5bd78d3 100644
--- a/app/src/main/java/org/oxycblt/auxio/image/CoverProvider.kt
+++ b/app/src/main/java/org/oxycblt/auxio/image/CoverProvider.kt
@@ -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
}
}
}
diff --git a/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt b/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt
index eb402545c..03f731618 100644
--- a/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt
+++ b/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt
@@ -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))
}
diff --git a/app/src/main/java/org/oxycblt/auxio/image/covers/CoverSilo.kt b/app/src/main/java/org/oxycblt/auxio/image/covers/CoverSilo.kt
index 9d7413f65..50ffa61d2 100644
--- a/app/src/main/java/org/oxycblt/auxio/image/covers/CoverSilo.kt
+++ b/app/src/main/java/org/oxycblt/auxio/image/covers/CoverSilo.kt
@@ -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)
}
}
}
diff --git a/app/src/main/java/org/oxycblt/auxio/image/covers/NullCovers.kt b/app/src/main/java/org/oxycblt/auxio/image/covers/NullCovers.kt
index 7aeb4da5d..cffa626d1 100644
--- a/app/src/main/java/org/oxycblt/auxio/image/covers/NullCovers.kt
+++ b/app/src/main/java/org/oxycblt/auxio/image/covers/NullCovers.kt
@@ -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 {
+ 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) {
context.coversDir().listFiles()?.forEach { it.deleteRecursively() }
diff --git a/app/src/main/java/org/oxycblt/auxio/image/covers/SettingCovers.kt b/app/src/main/java/org/oxycblt/auxio/image/covers/SettingCovers.kt
index 63e1dfd8d..e00cfdc2f 100644
--- a/app/src/main/java/org/oxycblt/auxio/image/covers/SettingCovers.kt
+++ b/app/src/main/java/org/oxycblt/auxio/image/covers/SettingCovers.kt
@@ -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
+
+ companion object {
+ fun immutable(context: Context): Covers =
+ 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 =
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))
}
diff --git a/app/src/main/java/org/oxycblt/auxio/image/covers/SiloedCovers.kt b/app/src/main/java/org/oxycblt/auxio/image/covers/SiloedCovers.kt
index 253d0dacf..856239698 100644
--- a/app/src/main/java/org/oxycblt/auxio/image/covers/SiloedCovers.kt
+++ b/app/src/main/java/org/oxycblt/auxio/image/covers/SiloedCovers.kt
@@ -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 {
- val coverId = SiloedCoverId.parse(id) ?: return ObtainResult.Miss()
- if (coverId.silo != silo) return ObtainResult.Miss()
+class BaseSiloedCovers(private val context: Context) : Covers {
+ override suspend fun obtain(id: String): CoverResult {
+ 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 {
+ override suspend fun obtain(id: String): CoverResult {
+ 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 {
+ override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult =
+ 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) {
fileCovers.cleanup(excluding.filterIsInstance().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)
}
}
diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt
index 3c7d614c0..1af17e660 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt
@@ -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)
diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt
index be518bfa2..2fae8d3ba 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt
@@ -40,6 +40,8 @@ interface MusicSettings : Settings {
var musicLocations: List
/** 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()
}
diff --git a/app/src/main/java/org/oxycblt/auxio/settings/categories/MusicPreferenceFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/categories/MusicPreferenceFragment.kt
index 8727aba3d..752dead1a 100644
--- a/app/src/main/java/org/oxycblt/auxio/settings/categories/MusicPreferenceFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/settings/categories/MusicPreferenceFragment.kt
@@ -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
+ }
+ }
}
}
diff --git a/app/src/main/java/org/oxycblt/auxio/ui/accent/Accent.kt b/app/src/main/java/org/oxycblt/auxio/ui/accent/Accent.kt
index b55801fbd..217206d98 100644
--- a/app/src/main/java/org/oxycblt/auxio/ui/accent/Accent.kt
+++ b/app/src/main/java/org/oxycblt/auxio/ui/accent/Accent.kt
@@ -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
)
diff --git a/app/src/main/res/values-ar-rIQ/strings.xml b/app/src/main/res/values-ar-rIQ/strings.xml
index ace598087..80439fe96 100644
--- a/app/src/main/res/values-ar-rIQ/strings.xml
+++ b/app/src/main/res/values-ar-rIQ/strings.xml
@@ -266,4 +266,4 @@
الفئات الخاصة بك ستضهر هنا.
اعادة تحميل مكتبة الموسيقى عند حصول تغيير(يتطلب تنبيه ثابت)
تحذير: استخدام هذا الاعداد قد ينتج عنه ان يتم تفسير بعض العلامات بشكل خاطئ مثل ان تحتوي على قيم متعددة. يمكن ان يتم حل هذا بتقديم الفواصل الغير مرغوبةبالشارحة الخلفية(\\).
-
\ No newline at end of file
+
diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml
index e6117970d..af1438fb0 100644
--- a/app/src/main/res/values-et/strings.xml
+++ b/app/src/main/res/values-et/strings.xml
@@ -230,7 +230,7 @@
Koma (,)
Semikoolon (;)
Pluss (+)
- Ampersand (&)
+ Ampersand (and-märk)
Seadista tähemärke, mis eraldavad siltides mitut väärtust
Kaldkriips (/)
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 (\\).
@@ -324,4 +324,4 @@
Sinu žanrid saavad olema nähtavad siin.
Sinu albumid saavad olema nähtavad siin.
Sinu esitusloendid saavad olema nähtavad siin.
-
\ No newline at end of file
+
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index a152d00e3..7d2fe3d74 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -152,9 +152,9 @@
Regolazione senza tag
Casuale
Tutto in casuale
- Regolazione mediante tag
+ Regolazione in base ai tag
Il pre-amp è applicato alla regolazione esistente durante la riproduzione
- Riproduci dall\'elemento mostrato
+ Riproduci dall\'elemento corrente
Gestisci le cartelle da dove caricare la musica
Cartelle musicali
Matroska audio
@@ -182,7 +182,7 @@
Ricarica la tua libreria musicale se subisce cambiamenti (richiede notifica persistente)
Caricamento musica
Monitoraggio libreria musicale
- Data aggiunta
+ Data di aggiunta
Ricaricamento automatico
EP
EP
@@ -219,7 +219,7 @@
Separatori multi-valore
Configura i caratteri che identificano tag con valori multipli
Barra (/)
- Attenzione: potrebbero verificarsi degli errori nell\'interpretazione di alcuni tag con valori multipli. Puoi risolvere aggiungendo come prefisso la barra rovesciata () ai separatori indesiderati.
+ 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.
E commerciale (&)
Compilation live
Compilation remix
@@ -334,4 +334,4 @@
MPEG-4 contenente %s
Apple Lossless Audio Codec (ALAC)
Sconosciuto
-
\ No newline at end of file
+
diff --git a/app/src/main/res/values-ka/strings.xml b/app/src/main/res/values-ka/strings.xml
index 954c3d4a9..1c02fbacb 100644
--- a/app/src/main/res/values-ka/strings.xml
+++ b/app/src/main/res/values-ka/strings.xml
@@ -19,4 +19,91 @@
პირდაპირი EP
სინგლები
სინგლი
+ Live ალბომები
+ Remix ალბომები
+ არტისტი
+ არტისტები
+ ჟანრი
+ ჟანრები
+ დასაკრავი სია
+ დასაკრავი სიები
+ ახალი დასაკრავი სია
+ ცარიელი დასაკრავი სია
+ იმპორტი
+ ექსპორტი
+ სახელის გადარქმევა
+ დასაკრავი სია იმპორტირებულია
+ დასაკრავი სიის იმპორტი
+ დასაკრავი სიის ექსპორტი
+ წაშლა
+ დასაკრავი სიის წაშლა ?
+ ჩასწორება
+ ძებნა
+ ფილტრი
+ ყველა
+ სახელი
+ დასაკრავი სიის სახელის შეცვლა
+ თარიღი
+ ხანგრძლივობა
+ დამატების თარიღი
+ დაალაგე
+ სიმღერების რაოდენობა
+ დაალაგე
+ ზრდადობით
+ კლებადობით
+ მიხედვით
+ დაკვრა
+ ეხლა იკვრება
+ შემდეგი
+ დამკვრელი სიაში დამატება
+ არტისტის ნახვა
+ ალბომის ნახვა
+ რიგი
+ შემთხვევითი მუსიკა
+ გაზიარება
+ ფორმატი
+ ნახვა
+ სიმღერის პარამეტრები
+ პარამეტრების ნახვა
+ ზომა
+ დამატება
+ მეტი
+ გაუქმება
+ შენახვა
+ ვერსია
+ აპლიკაციის შესახებ
+ დაკოპირებულია
+ ავტორი
+ ინფორმაცია
+ უკუკავშირი
+ ელექტრონული შეტყობინების გაგზავნა
+ დონაცია
+ სიმღერების ჩატვირთვა…
+ დაამატე რიგში
+ დასაკრავი სია იმპორტირებულია
+ დასაკრავი სიის სახელი შეცვლილია
+ ძებნა თქვენს ბიბლიოთეკაში…
+ ალბომები აქ გამოჩნდება.
+ არტისტები აქ გამოჩნდება.
+ თემა
+ ღია
+ მუქი
+ შავი თემა
+ შემდეგზე გადასვლა
+ ყველა სიმღერის დაკვრა
+ დასაკრავი სია წაშლილია
+ დასაკრავი სია ექსპორტირებულია
+ დასაკრავი სია შექმნილია
+ დამატებულია დასაკრავი სიაში
+ ავტომატური
+ სიმღერები აქ გამოჩნდება.
+ პარამეტრები
+ ფერები
+ ავტომატური ჩატვირთვა
+ სურათები
+ მუსიკის საქაღალდე
+ საქაღალდეები
+ ახალი საქაღალდე
+ სიმღერები ვერ მოიძებნა
+ ბოლო სიმღერაზე გადასვლა
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index dfd3baee3..fab6bfb0d 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -335,4 +335,4 @@
Tutaj pojawią się dodani artyści.
Tutaj pojawią się dodane playlisty.
Tutaj pojawią się dodane gatunki.
-
\ No newline at end of file
+
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 4ba7c30a5..e714bc71f 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -2,7 +2,7 @@
Tentar novamente
- Confirmar
+ Conceder
Gêneros
Artistas
Álbuns
@@ -91,7 +91,7 @@
Recarregar música
Retroceder a música antes de voltar para a anterior
O Auxio precisa de permissão para ler sua biblioteca de músicas
- Um reprodutor de música simples e racional para android.
+ Um reprodutor de música simples e racional para Android.
Carregando a sua biblioteca de músicas…
Ano
Duração
@@ -167,17 +167,17 @@
Álbum ao vivo
Trilhas sonoras
Trilha sonora
- Álbum remix
+ Álbum de remix
EP ao vivo
- Álbum de Remix
+ EP de remix
Single ao vivo
Monitorando alterações na sua biblioteca de músicas…
Abas da biblioteca
Gênero
Reproduzir do artista
Ajuste em faixas com metadados
- Carregando música
- Carregando música
+ Carregamento de músicas
+ Carregando músicas
Monitorando a biblioteca de músicas
Cantos arredondados
Pular para o próximo
@@ -219,9 +219,9 @@
Ignora arquivos de áudio que não sejam música, como podcasts
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 (\\).
Ignorar arquivos que não sejam músicas
- Capas de álbuns
- Desligado
- Rápido
+ Qualidade das capas de álbuns
+ Desativado
+ Equilibrado
Alta qualidade
Mixagens de DJ
Mixagem de DJ
@@ -332,6 +332,8 @@
Os seus artistas aparecerão aqui.
As suas playlists aparecerão aqui.
Os seus gêneros aparecerão aqui.
- Salvar espaço
+ Economizar espaço
Nova pasta
-
\ No newline at end of file
+ Ignorar arquivos e pastas que estão ocultos (por exemplo, .cache)
+ Ignorar arquivos ocultos
+
diff --git a/app/src/main/res/values/colors_ui_black.xml b/app/src/main/res/values/colors_ui_black.xml
new file mode 100644
index 000000000..4d0b01b47
--- /dev/null
+++ b/app/src/main/res/values/colors_ui_black.xml
@@ -0,0 +1,145 @@
+
+
+ #000000
+ #000000
+ #211b1a
+ #000000
+ #110c0c
+ #130e0e
+ #181413
+ #1e1918
+
+ #000000
+ #000000
+ #201b1b
+ #000000
+ #110c0d
+ #130e0f
+ #181414
+ #1e1819
+
+ #000000
+ #000000
+ #1f1b1e
+ #000000
+ #0f0c0f
+ #110e11
+ #171416
+ #1c191c
+
+ #000000
+ #000000
+ #1d1c1f
+ #000000
+ #0e0d10
+ #100f12
+ #161417
+ #1b1a1d
+
+ #000000
+ #000000
+ #1c1c1f
+ #000000
+ #0d0d10
+ #0f0f12
+ #141417
+ #1a1a1d
+
+ #000000
+ #000000
+ #1b1c1e
+ #000000
+ #0c0d0f
+ #0e1012
+ #131517
+ #191a1c
+
+ #000000
+ #000000
+ #1a1d1e
+ #000000
+ #0b0e0f
+ #0d1011
+ #121517
+ #181a1c
+
+ #000000
+ #000000
+ #1a1d1e
+ #000000
+ #0b0e0e
+ #0d1010
+ #121515
+ #181b1b
+
+ #000000
+ #000000
+ #1a1d1c
+ #000000
+ #0b0e0d
+ #0d100f
+ #121514
+ #181b1a
+
+ #000000
+ #000000
+ #1b1d1a
+ #000000
+ #0c0e0b
+ #0e100d
+ #131512
+ #191b18
+
+ #000000
+ #000000
+ #1b1d19
+ #000000
+ #0d0e0a
+ #0f100d
+ #141512
+ #191b16
+
+ #000000
+ #000000
+ #1c1d18
+ #000000
+ #0d0e09
+ #10100c
+ #141511
+ #1a1a16
+
+ #000000
+ #000000
+ #1f1c17
+ #000000
+ #100d09
+ #120f0b
+ #171410
+ #1d1a15
+
+ #000000
+ #000000
+ #201b18
+ #000000
+ #110d0a
+ #130f0c
+ #181411
+ #1d1916
+ #000000
+ #000000
+ #1e1c1b
+ #000000
+ #0f0d0d
+ #110f0f
+ #161414
+ #1b1a19
+
+ #000000
+ #000000
+ #1d1c1c
+ #000000
+ #0e0d0d
+ #100f0f
+ #151515
+ #1a1a1a
+
\ No newline at end of file
diff --git a/app/src/main/res/values/settings.xml b/app/src/main/res/values/settings.xml
index 901803a0c..eec560b35 100644
--- a/app/src/main/res/values/settings.xml
+++ b/app/src/main/res/values/settings.xml
@@ -18,6 +18,7 @@
auxio_square_covers
auxio_include_dirs
auxio_exclude_non_music
+ auxio_with_hidden
auxio_music_locations2
auxio_separators
auxio_auto_sort_names
@@ -78,6 +79,7 @@
- @string/set_cover_mode_save_space
- @string/set_cover_mode_balanced
- @string/set_cover_mode_high_quality
+ - @string/set_cover_mode_as_is
@@ -85,6 +87,7 @@
- @integer/cover_mode_save_space
- @integer/cover_mode_balanced
- @integer/cover_mode_high_quality
+ - @integer/cover_mode_as_is
@@ -181,4 +184,5 @@
0xA125
0xA11D
0xA11E
+ 0xA126
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 37e68cc51..c055b0dd0 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -267,6 +267,8 @@
Reload the music library whenever it changes (requires persistent notification)
Exclude non-music
Ignore audio files that are not music, such as podcasts
+ Include hidden files
+ Include files and folders that are hidden (ex. .cache)
Multi-value separators
Configure characters that denote multiple tag values
Comma (,)
@@ -285,6 +287,7 @@
Save space
Balanced
High quality
+ As is
Force square album covers
Crop all album covers to a 1:1 aspect ratio
diff --git a/app/src/main/res/values/themes_black.xml b/app/src/main/res/values/themes_black.xml
index ed68ba1f9..75b8f6d24 100644
--- a/app/src/main/res/values/themes_black.xml
+++ b/app/src/main/res/values/themes_black.xml
@@ -1,71 +1,173 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/xml/preferences_music.xml b/app/src/main/res/xml/preferences_music.xml
index 607f2c7f2..b3b5106f5 100644
--- a/app/src/main/res/xml/preferences_music.xml
+++ b/app/src/main/res/xml/preferences_music.xml
@@ -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" />
+
+
-
\ No newline at end of file
+
diff --git a/fastlane/metadata/android/cs/full_description.txt b/fastlane/metadata/android/cs/full_description.txt
index e1e6af29c..04831f786 100644
--- a/fastlane/metadata/android/cs/full_description.txt
+++ b/fastlane/metadata/android/cs/full_description.txt
@@ -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)
diff --git a/fastlane/metadata/android/de/full_description.txt b/fastlane/metadata/android/de/full_description.txt
index 62f87e1ad..97df9a418 100644
--- a/fastlane/metadata/android/de/full_description.txt
+++ b/fastlane/metadata/android/de/full_description.txt
@@ -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)
diff --git a/fastlane/metadata/android/en-US/changelogs/61.txt b/fastlane/metadata/android/en-US/changelogs/61.txt
new file mode 100644
index 000000000..41e1fa8aa
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/61.txt
@@ -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.
diff --git a/fastlane/metadata/android/en-US/images/featureGraphic.png b/fastlane/metadata/android/en-US/images/featureGraphic.png
index 52a9ff852..55e7b0342 100644
Binary files a/fastlane/metadata/android/en-US/images/featureGraphic.png and b/fastlane/metadata/android/en-US/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot0.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot0.png
index 6657a9e38..fad7362d1 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot0.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot0.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot1.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot1.png
index fc7667973..6dd7269be 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot1.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot1.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot2.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot2.png
index 8b71fa259..fbc3bf201 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot2.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot2.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot3.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot3.png
index ac44384b7..5bdbb9a3b 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot3.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot3.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot4.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot4.png
index 2a5d413a3..8e62d14b4 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot4.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot4.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot5.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot5.png
index 0dc12f9d7..a5a09ae13 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot5.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot5.png differ
diff --git a/fastlane/metadata/android/et/full_description.txt b/fastlane/metadata/android/et/full_description.txt
index 14417fef1..4fe18fc3c 100644
--- a/fastlane/metadata/android/et/full_description.txt
+++ b/fastlane/metadata/android/et/full_description.txt
@@ -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)
diff --git a/fastlane/metadata/android/hr/full_description.txt b/fastlane/metadata/android/hr/full_description.txt
index 0d40db862..a5c141a11 100644
--- a/fastlane/metadata/android/hr/full_description.txt
+++ b/fastlane/metadata/android/hr/full_description.txt
@@ -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, Reproducira glazbu.
+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, Reproducira glazbu..
Značajke
@@ -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)
diff --git a/fastlane/metadata/android/it/short_description.txt b/fastlane/metadata/android/it/short_description.txt
index ccbd35600..39fc5a513 100644
--- a/fastlane/metadata/android/it/short_description.txt
+++ b/fastlane/metadata/android/it/short_description.txt
@@ -1 +1 @@
-Un semplice, razionale lettore musicale
+Un lettore musicale semplice e razionale
diff --git a/fastlane/metadata/android/pt-BR/full_description.txt b/fastlane/metadata/android/pt-BR/full_description.txt
index 3c263cbcf..162bca762 100644
--- a/fastlane/metadata/android/pt-BR/full_description.txt
+++ b/fastlane/metadata/android/pt-BR/full_description.txt
@@ -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).
diff --git a/fastlane/metadata/android/uk/full_description.txt b/fastlane/metadata/android/uk/full_description.txt
index 4fbd4baca..742c702c6 100644
--- a/fastlane/metadata/android/uk/full_description.txt
+++ b/fastlane/metadata/android/uk/full_description.txt
@@ -6,8 +6,7 @@ Auxio — це локальний музичний плеєр із швидки
- Snappy UI, створений на основі останніх рекомендацій Material Design
- Переконливий UX, який надає перевагу простоті використання над крайніми випадками
- Настроювана поведінка
-- Підтримка номерів дисків, кількох виконавців, типів випусків,
-точні/оригінальні дати, теги сортування тощо
+- Підтримка номерів дисків, кількох виконавців, типів випусків, точних/оригінальних дат, тегів сортування тощо
— Розширена система виконавців, яка об’єднує виконавців і виконавців альбомів
- Керування папками з підтримкою SD-карти
— Надійна функція списків відтворення
@@ -22,4 +21,4 @@ Auxio — це локальний музичний плеєр із швидки
- Автовідтворення гарнітури
- Стильні віджети, які автоматично адаптуються до їх розміру
- Повністю приватний і офлайн
-- Немає округлених обкладинок альбомів (за замовчуванням)
+- Немає закруглених обкладинок альбомів (якщо ви їх хочете)
diff --git a/fastlane/metadata/android/zh-CN/full_description.txt b/fastlane/metadata/android/zh-CN/full_description.txt
index cc2ef74c1..96ed1d7b8 100644
--- a/fastlane/metadata/android/zh-CN/full_description.txt
+++ b/fastlane/metadata/android/zh-CN/full_description.txt
@@ -6,8 +6,7 @@ Auxio 是一款本地音乐播放器,它拥有快速、可靠的 UI/UX,没
- 源于最新 Material You 设计规范的灵动界面
- 优先考虑易用性的独到用户体验
- 可定制的播放器行为
-- 支持唱片编号、多名艺术家、发布类型、精确/原始日期、
-标签排序及其他更多功能
+- 支持唱片编号、多名艺术家、发行类型、精确/原始日期、排序标签以及更多
- 统一“艺术家”和“专辑艺术家”的高级“艺术家”系统
- 文件夹管理功能可以感知到 SD 卡
- 可靠的播放列表功能
@@ -22,4 +21,4 @@ Auxio 是一款本地音乐播放器,它拥有快速、可靠的 UI/UX,没
- 耳机连接时自动播放
- 按桌面尺寸自适应的风格化微件
- 完全离线且私密
-- 没有圆角的专辑封面(默认设置)
+- 没有圆角的专辑封面(即使你想要)
diff --git a/musikr/src/main/cpp/taglib_jni.cpp b/musikr/src/main/cpp/taglib_jni.cpp
index abb94e9f7..65904aa5c 100644
--- a/musikr/src/main/cpp/taglib_jni.cpp
+++ b/musikr/src/main/cpp/taglib_jni.cpp
@@ -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(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(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(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(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(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(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;
}
}
diff --git a/musikr/src/main/java/org/oxycblt/musikr/Config.kt b/musikr/src/main/java/org/oxycblt/musikr/Config.kt
index a793680db..6cded8171 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/Config.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/Config.kt
@@ -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,
/**
* 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
)
diff --git a/musikr/src/main/java/org/oxycblt/musikr/Musikr.kt b/musikr/src/main/java/org/oxycblt/musikr/Musikr.kt
index c18a01684..934880963 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/Musikr.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/Musikr.kt
@@ -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))
}
diff --git a/musikr/src/main/java/org/oxycblt/musikr/cache/Cache.kt b/musikr/src/main/java/org/oxycblt/musikr/cache/Cache.kt
index 277495d3a..670ee2f31 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/cache/Cache.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/cache/Cache.kt
@@ -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): CacheResult
internal abstract suspend fun write(song: RawSong)
diff --git a/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt b/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt
index d1700777c..7fc755f1d 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt
@@ -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): RawSong? {
val cover =
when (val result = coverId?.let { covers.obtain(it) }) {
// We found the cover.
- is ObtainResult.Hit -> result.cover
+ is CoverResult.Hit -> result.cover
// We actually didn't find the cover, can't safely convert.
- is ObtainResult.Miss -> return null
+ is CoverResult.Miss -> return null
// No cover in the first place, can ignore.
null -> null
}
diff --git a/musikr/src/main/java/org/oxycblt/musikr/cache/StoredCache.kt b/musikr/src/main/java/org/oxycblt/musikr/cache/StoredCache.kt
index 4707ffe3f..c4107c3a5 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/cache/StoredCache.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/cache/StoredCache.kt
@@ -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): 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) =
CacheResult.Miss(file, invisibleCacheDao.selectAddedMs(file.uri.toString()))
class Factory(private val cacheDatabase: CacheDatabase) : Cache.Factory() {
diff --git a/musikr/src/main/java/org/oxycblt/musikr/cover/CoverFormat.kt b/musikr/src/main/java/org/oxycblt/musikr/cover/CoverFormat.kt
index 52f9e15e3..16af47506 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/cover/CoverFormat.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/cover/CoverFormat.kt
@@ -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
+ }
+ }
+}
diff --git a/musikr/src/main/java/org/oxycblt/musikr/cover/Covers.kt b/musikr/src/main/java/org/oxycblt/musikr/cover/Covers.kt
index e3f41b386..902bce5c5 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/cover/Covers.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/cover/Covers.kt
@@ -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
+interface Covers {
+ suspend fun obtain(id: String): CoverResult
+
+ companion object {
+ fun chain(vararg many: Covers): Covers =
+ object : Covers {
+ override suspend fun obtain(id: String): CoverResult {
+ 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 : Covers {
+ suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult
suspend fun cleanup(excluding: Collection)
+
+ companion object {
+ fun chain(vararg many: MutableCovers): MutableCovers =
+ object : MutableCovers {
+ override suspend fun obtain(id: String): CoverResult {
+ 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 {
+ 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) {
+ for (cover in many) {
+ cover.cleanup(excluding)
+ }
+ }
+ }
+ }
}
-sealed interface ObtainResult {
- data class Hit(val cover: T) : ObtainResult
+sealed interface CoverResult {
+ data class Hit(val cover: T) : CoverResult
- class Miss : ObtainResult
+ class Miss : CoverResult
}
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) {
diff --git a/musikr/src/main/java/org/oxycblt/musikr/cover/FileCovers.kt b/musikr/src/main/java/org/oxycblt/musikr/cover/FileCovers.kt
index 4c8390869..aadafad1d 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/cover/FileCovers.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/cover/FileCovers.kt
@@ -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 {
+ Covers {
+ override suspend fun obtain(id: String): CoverResult {
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 {
+ override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult {
+ 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) {
diff --git a/musikr/src/main/java/org/oxycblt/musikr/cover/FolderCovers.kt b/musikr/src/main/java/org/oxycblt/musikr/cover/FolderCovers.kt
new file mode 100644
index 000000000..46db555e6
--- /dev/null
+++ b/musikr/src/main/java/org/oxycblt/musikr/cover/FolderCovers.kt
@@ -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 .
+ */
+
+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 {
+ override suspend fun obtain(id: String): CoverResult {
+ // 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 {
+ override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult {
+ 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) {
+ // 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) }
+}
diff --git a/musikr/src/main/java/org/oxycblt/musikr/fs/DeviceFile.kt b/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFile.kt
similarity index 60%
rename from musikr/src/main/java/org/oxycblt/musikr/fs/DeviceFile.kt
rename to musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFile.kt
index 6baac772f..cee785cb0 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/fs/DeviceFile.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFile.kt
@@ -16,14 +16,29 @@
* along with this program. If not, see .
*/
-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?,
+ val children: List
+) : 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
+) : DeviceNode
diff --git a/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFiles.kt b/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFiles.kt
index 2f737995c..5ee0d3417 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFiles.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFiles.kt
@@ -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): Flow
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): Flow =
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?
): Flow = 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>()
- 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()
+ val recursive = mutableListOf>()
+ val children = mutableListOf()
+ 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 {
diff --git a/musikr/src/main/java/org/oxycblt/musikr/graph/MusicGraph.kt b/musikr/src/main/java/org/oxycblt/musikr/graph/MusicGraph.kt
index fc9420618..2b634ae5e 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/graph/MusicGraph.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/graph/MusicGraph.kt
@@ -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 }
}
}
diff --git a/musikr/src/main/java/org/oxycblt/musikr/metadata/Metadata.kt b/musikr/src/main/java/org/oxycblt/musikr/metadata/Metadata.kt
index 758e4483a..ba16830b5 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/metadata/Metadata.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/metadata/Metadata.kt
@@ -18,7 +18,7 @@
package org.oxycblt.musikr.metadata
-internal data class Metadata(
+data class Metadata(
val id3v2: Map>,
val xiph: Map>,
val mp4: Map>,
@@ -53,7 +53,7 @@ internal data class Metadata(
}
}
-internal data class Properties(
+data class Properties(
val mimeType: String,
val durationMs: Long,
val bitrateKbps: Int,
diff --git a/musikr/src/main/java/org/oxycblt/musikr/metadata/MetadataExtractor.kt b/musikr/src/main/java/org/oxycblt/musikr/metadata/MetadataExtractor.kt
index 0b0cfc48d..7ce64d949 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/metadata/MetadataExtractor.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/metadata/MetadataExtractor.kt
@@ -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?
diff --git a/musikr/src/main/java/org/oxycblt/musikr/metadata/NativeInputStream.kt b/musikr/src/main/java/org/oxycblt/musikr/metadata/NativeInputStream.kt
index c7486e220..213e68ea7 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/metadata/NativeInputStream.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/metadata/NativeInputStream.kt
@@ -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
diff --git a/musikr/src/main/java/org/oxycblt/musikr/metadata/TagLibJNI.kt b/musikr/src/main/java/org/oxycblt/musikr/metadata/TagLibJNI.kt
index d5105f3a8..e0b48ee00 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/metadata/TagLibJNI.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/metadata/TagLibJNI.kt
@@ -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 {
diff --git a/musikr/src/main/java/org/oxycblt/musikr/model/LibraryImpl.kt b/musikr/src/main/java/org/oxycblt/musikr/model/LibraryImpl.kt
index e60d4f662..381be7290 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/model/LibraryImpl.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/model/LibraryImpl.kt
@@ -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 }
diff --git a/musikr/src/main/java/org/oxycblt/musikr/model/SongImpl.kt b/musikr/src/main/java/org/oxycblt/musikr/model/SongImpl.kt
index 383abf11c..c06e5d0e1 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/model/SongImpl.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/model/SongImpl.kt
@@ -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
diff --git a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt
index 1e77659e1..761b0d047 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt
@@ -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): Flow
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 =
diff --git a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt
index 1d204fe5a..576980b3a 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt
@@ -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
) : ExtractStep {
@OptIn(ExperimentalCoroutinesApi::class)
override fun extract(nodes: Flow): Flow {
@@ -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(val file: DeviceFile, val with: T)
}
internal data class RawSong(
diff --git a/musikr/src/main/java/org/oxycblt/musikr/pipeline/PipelineException.kt b/musikr/src/main/java/org/oxycblt/musikr/pipeline/PipelineException.kt
index 1f6efc892..65d7525f5 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/PipelineException.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/PipelineException.kt
@@ -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
diff --git a/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/PreMusic.kt b/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/PreMusic.kt
index d5722b0a6..3c744d51e 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/PreMusic.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/PreMusic.kt
@@ -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,
diff --git a/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/TagInterpreter.kt b/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/TagInterpreter.kt
index de6f07a49..eb2787e7a 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/TagInterpreter.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/TagInterpreter.kt
@@ -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,