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..24fccbd16 100644
--- a/README.md
+++ b/README.md
@@ -2,8 +2,8 @@
Auxio
A simple, rational music player for android.
-
-
+
+
@@ -15,7 +15,12 @@
-
+
+
+
+
+
+
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/image/CoverProvider.kt b/app/src/main/java/org/oxycblt/auxio/image/CoverProvider.kt
index 5c5bd78d3..160267804 100644
--- a/app/src/main/java/org/oxycblt/auxio/image/CoverProvider.kt
+++ b/app/src/main/java/org/oxycblt/auxio/image/CoverProvider.kt
@@ -28,7 +28,7 @@ import android.os.ParcelFileDescriptor
import kotlinx.coroutines.runBlocking
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.image.covers.SettingCovers
-import org.oxycblt.musikr.cover.CoverResult
+import org.oxycblt.musikr.covers.CoverResult
class CoverProvider() : ContentProvider() {
override fun onCreate(): Boolean = true
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 03f731618..bab90cbe6 100644
--- a/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt
+++ b/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt
@@ -64,7 +64,7 @@ import org.oxycblt.musikr.Artist
import org.oxycblt.musikr.Genre
import org.oxycblt.musikr.Playlist
import org.oxycblt.musikr.Song
-import org.oxycblt.musikr.cover.CoverCollection
+import org.oxycblt.musikr.covers.CoverCollection
/**
* Auxio's extension of [ImageView] that enables cover art loading and playing indicator and
diff --git a/app/src/main/java/org/oxycblt/auxio/image/coil/CoverCollectionFetcher.kt b/app/src/main/java/org/oxycblt/auxio/image/coil/CoverCollectionFetcher.kt
index 147721fbb..a6bc9475d 100644
--- a/app/src/main/java/org/oxycblt/auxio/image/coil/CoverCollectionFetcher.kt
+++ b/app/src/main/java/org/oxycblt/auxio/image/coil/CoverCollectionFetcher.kt
@@ -46,7 +46,7 @@ import kotlinx.coroutines.withContext
import okio.FileSystem
import okio.buffer
import okio.source
-import org.oxycblt.musikr.cover.CoverCollection
+import org.oxycblt.musikr.covers.CoverCollection
class CoverCollectionFetcher
private constructor(
diff --git a/app/src/main/java/org/oxycblt/auxio/image/coil/CoverFetcher.kt b/app/src/main/java/org/oxycblt/auxio/image/coil/CoverFetcher.kt
index 29bbb430b..4c0ed0c5c 100644
--- a/app/src/main/java/org/oxycblt/auxio/image/coil/CoverFetcher.kt
+++ b/app/src/main/java/org/oxycblt/auxio/image/coil/CoverFetcher.kt
@@ -40,7 +40,7 @@ import javax.inject.Inject
import okio.FileSystem
import okio.buffer
import okio.source
-import org.oxycblt.musikr.cover.Cover
+import org.oxycblt.musikr.covers.Cover
class CoverFetcher private constructor(private val context: Context, private val cover: Cover) :
Fetcher {
diff --git a/app/src/main/java/org/oxycblt/auxio/image/coil/Keyers.kt b/app/src/main/java/org/oxycblt/auxio/image/coil/Keyers.kt
index 614b1bdf1..cd90be0f8 100644
--- a/app/src/main/java/org/oxycblt/auxio/image/coil/Keyers.kt
+++ b/app/src/main/java/org/oxycblt/auxio/image/coil/Keyers.kt
@@ -21,8 +21,8 @@ package org.oxycblt.auxio.image.coil
import coil3.key.Keyer
import coil3.request.Options
import javax.inject.Inject
-import org.oxycblt.musikr.cover.Cover
-import org.oxycblt.musikr.cover.CoverCollection
+import org.oxycblt.musikr.covers.Cover
+import org.oxycblt.musikr.covers.CoverCollection
class CoverKeyer @Inject constructor() : Keyer {
override fun key(data: Cover, options: Options) = "${data.id}&${options.size}"
diff --git a/app/src/main/java/org/oxycblt/auxio/image/covers/CoverModule.kt b/app/src/main/java/org/oxycblt/auxio/image/covers/CoverModule.kt
index 48f5d1052..714dd1994 100644
--- a/app/src/main/java/org/oxycblt/auxio/image/covers/CoverModule.kt
+++ b/app/src/main/java/org/oxycblt/auxio/image/covers/CoverModule.kt
@@ -23,7 +23,7 @@ import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
-import org.oxycblt.musikr.cover.CoverIdentifier
+import org.oxycblt.musikr.covers.embedded.CoverIdentifier
@Module
@InstallIn(SingletonComponent::class)
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 9bd93874d..a5383621b 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
@@ -19,20 +19,25 @@
package org.oxycblt.auxio.image.covers
import java.util.UUID
-import org.oxycblt.musikr.cover.CoverParams
+import org.oxycblt.musikr.covers.embedded.CoverParams
data class CoverSilo(val revision: UUID, val params: CoverParams?) {
override fun toString() =
- "${revision}.${params?.let { "${params.resolution}${params.quality}" }}"
+ "${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 cffa626d1..079ed5f6c 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
@@ -19,9 +19,9 @@
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.covers.Cover
+import org.oxycblt.musikr.covers.CoverResult
+import org.oxycblt.musikr.covers.MutableCovers
import org.oxycblt.musikr.fs.device.DeviceFile
import org.oxycblt.musikr.metadata.Metadata
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 e00cfdc2f..2f4629451 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,21 +23,21 @@ 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
+import org.oxycblt.musikr.covers.Cover
+import org.oxycblt.musikr.covers.Covers
+import org.oxycblt.musikr.covers.MutableCovers
+import org.oxycblt.musikr.covers.fs.FSCovers
+import org.oxycblt.musikr.covers.fs.MutableFSCovers
+import org.oxycblt.musikr.covers.embedded.CoverIdentifier
+import org.oxycblt.musikr.covers.embedded.CoverParams
+import org.oxycblt.musikr.covers.embedded.FileCover
interface SettingCovers {
suspend fun mutate(context: Context, revision: UUID): MutableCovers
companion object {
fun immutable(context: Context): Covers =
- Covers.chain(BaseSiloedCovers(context), FolderCovers(context))
+ Covers.chain(BaseSiloedCovers(context), FSCovers(context))
}
}
@@ -57,5 +57,5 @@ constructor(private val imageSettings: ImageSettings, private val identifier: Co
private suspend fun siloedCovers(context: Context, revision: UUID, with: CoverParams?) =
MutableCovers.chain(
MutableSiloedCovers.from(context, CoverSilo(revision, with), identifier),
- MutableFolderCovers(context))
+ MutableFSCovers(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 856239698..e3dd33f84 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
@@ -22,16 +22,16 @@ import android.content.Context
import java.io.File
import kotlinx.coroutines.Dispatchers
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.fs.app.AppFiles
+import org.oxycblt.musikr.covers.Cover
+import org.oxycblt.musikr.covers.CoverResult
+import org.oxycblt.musikr.covers.Covers
+import org.oxycblt.musikr.covers.MutableCovers
+import org.oxycblt.musikr.covers.embedded.CoverFormat
+import org.oxycblt.musikr.covers.embedded.CoverIdentifier
+import org.oxycblt.musikr.covers.embedded.FileCover
+import org.oxycblt.musikr.covers.embedded.EmbeddedCovers
+import org.oxycblt.musikr.covers.embedded.MutableEmbeddedCovers
+import org.oxycblt.musikr.fs.app.AppFS
import org.oxycblt.musikr.fs.device.DeviceFile
import org.oxycblt.musikr.metadata.Metadata
@@ -39,20 +39,20 @@ 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)) {
+ val embeddedCovers = EmbeddedCovers(core.files, core.format)
+ return when (val result = embeddedCovers.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) :
+open class SiloedCovers(private val silo: CoverSilo, private val embeddedCovers: EmbeddedCovers) :
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)) {
+ return when (val result = embeddedCovers.obtain(coverId.id)) {
is CoverResult.Hit -> CoverResult.Hit(SiloedCover(silo, result.cover))
is CoverResult.Miss -> CoverResult.Miss()
}
@@ -61,7 +61,7 @@ open class SiloedCovers(private val silo: CoverSilo, private val fileCovers: Fil
companion object {
suspend fun from(context: Context, silo: CoverSilo): SiloedCovers {
val core = SiloCore.from(context, silo)
- return SiloedCovers(silo, FileCovers(core.files, core.format))
+ return SiloedCovers(silo, EmbeddedCovers(core.files, core.format))
}
}
}
@@ -70,7 +70,7 @@ class MutableSiloedCovers
private constructor(
private val rootDir: File,
private val silo: CoverSilo,
- private val fileCovers: MutableFileCovers
+ private val fileCovers: MutableEmbeddedCovers
) : SiloedCovers(silo, fileCovers), MutableCovers {
override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult =
when (val result = fileCovers.create(file, metadata)) {
@@ -96,7 +96,7 @@ private constructor(
): MutableSiloedCovers {
val core = SiloCore.from(context, silo)
return MutableSiloedCovers(
- core.rootDir, silo, MutableFileCovers(core.files, core.format, coverIdentifier))
+ core.rootDir, silo, MutableEmbeddedCovers(core.files, core.format, coverIdentifier))
}
}
}
@@ -120,7 +120,7 @@ data class SiloedCoverId(val silo: CoverSilo, val id: String) {
}
}
-private data class SiloCore(val rootDir: File, val files: AppFiles, val format: CoverFormat) {
+private data class SiloCore(val rootDir: File, val files: AppFS, val format: CoverFormat) {
companion object {
suspend fun from(context: Context, silo: CoverSilo): SiloCore {
val rootDir: File
@@ -129,7 +129,7 @@ private data class SiloCore(val rootDir: File, val files: AppFiles, val format:
rootDir = context.coversDir()
revisionDir = rootDir.resolve(silo.toString()).apply { mkdirs() }
}
- val files = AppFiles.at(revisionDir)
+ val files = AppFS.at(revisionDir)
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 90aac5c36..59a09e91d 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt
@@ -29,6 +29,7 @@ import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield
import org.oxycblt.auxio.image.covers.SettingCovers
import org.oxycblt.auxio.music.MusicRepository.IndexingWorker
+import org.oxycblt.auxio.music.shim.WriteOnlyMutableCache
import org.oxycblt.musikr.IndexingProgress
import org.oxycblt.musikr.Interpretation
import org.oxycblt.musikr.Library
@@ -38,7 +39,7 @@ import org.oxycblt.musikr.MutableLibrary
import org.oxycblt.musikr.Playlist
import org.oxycblt.musikr.Song
import org.oxycblt.musikr.Storage
-import org.oxycblt.musikr.cache.StoredCache
+import org.oxycblt.musikr.cache.db.MutableDBCache
import org.oxycblt.musikr.playlist.db.StoredPlaylists
import org.oxycblt.musikr.tag.interpret.Naming
import org.oxycblt.musikr.tag.interpret.Separators
@@ -236,7 +237,7 @@ class MusicRepositoryImpl
@Inject
constructor(
@ApplicationContext private val context: Context,
- private val storedCache: StoredCache,
+ private val dbCache: MutableDBCache,
private val storedPlaylists: StoredPlaylists,
private val settingCovers: SettingCovers,
private val musicSettings: MusicSettings
@@ -384,15 +385,14 @@ constructor(
Naming.simple()
}
val locations = musicSettings.musicLocations
- val ignoreHidden = musicSettings.ignoreHidden
+ val withHidden = musicSettings.withHidden
val currentRevision = musicSettings.revision
val newRevision = currentRevision?.takeIf { withCache } ?: UUID.randomUUID()
- val cache = if (withCache) storedCache.visible() else storedCache.invisible()
+ val cache = if (withCache) dbCache else WriteOnlyMutableCache(dbCache)
val covers = settingCovers.mutate(context, newRevision)
val storage = Storage(cache, covers, storedPlaylists)
- val interpretation = Interpretation(nameFactory, separators, ignoreHidden)
-
+ val interpretation = Interpretation(nameFactory, separators, withHidden)
val result =
Musikr.new(context, storage, interpretation).run(locations, ::emitIndexingProgress)
// Music loading completed, update the revision right now so we re-use this work
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 d94d6ec4c..8ae954e5a 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt
@@ -41,7 +41,7 @@ interface MusicSettings : Settings {
/** 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 ignoreHidden: Boolean
+ 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. */
@@ -92,8 +92,8 @@ 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 ignoreHidden: Boolean
- get() = sharedPreferences.getBoolean(getString(R.string.set_key_ignore_hidden), true)
+ override val withHidden: Boolean
+ get() = sharedPreferences.getBoolean(getString(R.string.set_key_with_hidden), true)
override val shouldBeObserving: Boolean
get() = sharedPreferences.getBoolean(getString(R.string.set_key_observing), false)
@@ -122,7 +122,7 @@ class MusicSettingsImpl @Inject constructor(@ApplicationContext private val cont
}
getString(R.string.set_key_separators),
getString(R.string.set_key_auto_sort_names),
- getString(R.string.set_key_ignore_hidden),
+ 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/music/shim/MusikrShimModule.kt b/app/src/main/java/org/oxycblt/auxio/music/shim/MusikrShimModule.kt
index 977936cd5..ee0702450 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/shim/MusikrShimModule.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/shim/MusikrShimModule.kt
@@ -25,7 +25,7 @@ import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
-import org.oxycblt.musikr.cache.StoredCache
+import org.oxycblt.musikr.cache.db.MutableDBCache
import org.oxycblt.musikr.playlist.db.StoredPlaylists
@Module
@@ -33,7 +33,7 @@ import org.oxycblt.musikr.playlist.db.StoredPlaylists
class MusikrShimModule {
@Singleton
@Provides
- fun storedCache(@ApplicationContext context: Context) = StoredCache.from(context)
+ fun cache(@ApplicationContext context: Context) = MutableDBCache.from(context)
@Singleton
@Provides
diff --git a/app/src/main/java/org/oxycblt/auxio/music/shim/WriteOnlyMutableCache.kt b/app/src/main/java/org/oxycblt/auxio/music/shim/WriteOnlyMutableCache.kt
new file mode 100644
index 000000000..d4076f965
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/auxio/music/shim/WriteOnlyMutableCache.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2025 Auxio Project
+ * WriteOnlyMutableCache.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.auxio.music.shim
+
+import org.oxycblt.musikr.cache.CacheResult
+import org.oxycblt.musikr.cache.CachedSong
+import org.oxycblt.musikr.cache.MutableCache
+import org.oxycblt.musikr.fs.device.DeviceFile
+
+class WriteOnlyMutableCache(private val inner: MutableCache) : MutableCache {
+ override suspend fun read(file: DeviceFile): CacheResult {
+ return when (val result = inner.read(file)) {
+ is CacheResult.Hit -> CacheResult.Stale(file, result.song.addedMs)
+ else -> result
+ }
+ }
+
+ override suspend fun write(cachedSong: CachedSong) {
+ inner.write(cachedSong)
+ }
+
+ override suspend fun cleanup(excluding: List) {
+ inner.cleanup(excluding)
+ }
+}
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 9707afae0..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,7 +67,7 @@ class MusicPreferenceFragment : BasePreferenceFragment(R.xml.preferences_music)
true
}
}
- if (preference.key == getString(R.string.set_key_ignore_hidden)) {
+ if (preference.key == getString(R.string.set_key_with_hidden)) {
L.d("Configuring ignore hidden files setting")
preference.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _, _ ->
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-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index f1dcb8e5e..a9f823aba 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -334,7 +334,7 @@
Os seus gêneros aparecerão aqui.
Economizar espaço
Nova pasta
- Ignorar arquivos e pastas que estão ocultos (por exemplo, .cache)
- Ignorar arquivos ocultos
Qualidade original
+ 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 99438b0d6..eec560b35 100644
--- a/app/src/main/res/values/settings.xml
+++ b/app/src/main/res/values/settings.xml
@@ -18,7 +18,7 @@
auxio_square_covers
auxio_include_dirs
auxio_exclude_non_music
- auxio_ignore_hidden
+ auxio_with_hidden
auxio_music_locations2
auxio_separators
auxio_auto_sort_names
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 64bbcfd36..164034bfc 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -267,8 +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
- Ignore hidden files
- Skip files and folders that are hidden (ex. .cache)
+ Include hidden files
+ Include audio files that are hidden (ex. .cache)
Multi-value separators
Configure characters that denote multiple tag values
Comma (,)
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 d164aed5c..e899ace74 100644
--- a/app/src/main/res/xml/preferences_music.xml
+++ b/app/src/main/res/xml/preferences_music.xml
@@ -17,9 +17,9 @@
+ app:key="@string/set_key_with_hidden"
+ app:summary="@string/set_with_hidden_desc"
+ app:title="@string/set_with_hidden" />
(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 d00b3b052..df7c5cf07 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/Config.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/Config.kt
@@ -18,9 +18,9 @@
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.cache.MutableCache
+import org.oxycblt.musikr.covers.Cover
+import org.oxycblt.musikr.covers.MutableCovers
import org.oxycblt.musikr.playlist.db.StoredPlaylists
import org.oxycblt.musikr.tag.interpret.Naming
import org.oxycblt.musikr.tag.interpret.Separators
@@ -28,17 +28,17 @@ import org.oxycblt.musikr.tag.interpret.Separators
/** Side-effect laden [Storage] for use during music loading and [MutableLibrary] operation. */
data class Storage(
/**
- * A factory producing a repository of cached metadata to read and write from over the course of
- * music loading. This will only be used during music loading.
+ * A repository of cached metadata to read and write from over the course of music loading. This
+ * will only be used during music loading.
*/
- val cache: Cache.Factory,
+ val cache: MutableCache,
/**
* A repository of cover images to for re-use during music loading. Should be kept in lock-step
* 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 covers: MutableCovers,
/**
* A repository of user-created playlists that should also be loaded into the library. This will
@@ -56,6 +56,6 @@ data class Interpretation(
/** What separators delimit multi-value audio tags. */
val separators: Separators,
- /** Whether to ignore hidden files and directories (those starting with a dot). */
- val ignoreHidden: Boolean = true
+ /** Whether to include hidden files and directories (those starting with a dot). */
+ val withHidden: Boolean
)
diff --git a/musikr/src/main/java/org/oxycblt/musikr/Music.kt b/musikr/src/main/java/org/oxycblt/musikr/Music.kt
index 5ae85f2d2..507103898 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/Music.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/Music.kt
@@ -25,8 +25,8 @@ import java.security.MessageDigest
import java.util.UUID
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
-import org.oxycblt.musikr.cover.Cover
-import org.oxycblt.musikr.cover.CoverCollection
+import org.oxycblt.musikr.covers.Cover
+import org.oxycblt.musikr.covers.CoverCollection
import org.oxycblt.musikr.fs.Format
import org.oxycblt.musikr.fs.Path
import org.oxycblt.musikr.tag.Date
diff --git a/musikr/src/main/java/org/oxycblt/musikr/Musikr.kt b/musikr/src/main/java/org/oxycblt/musikr/Musikr.kt
index c18a01684..27b84f1f2 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))
}
@@ -143,6 +143,6 @@ private class LibraryResultImpl(
override val library: MutableLibrary
) : LibraryResult {
override suspend fun cleanup() {
- storage.storedCovers.cleanup(library.songs.mapNotNull { it.cover })
+ storage.covers.cleanup(library.songs.mapNotNull { it.cover })
}
}
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 670ee2f31..fe1fbb42c 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/cache/Cache.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/cache/Cache.kt
@@ -18,25 +18,32 @@
package org.oxycblt.musikr.cache
-import org.oxycblt.musikr.cover.Cover
-import org.oxycblt.musikr.cover.Covers
import org.oxycblt.musikr.fs.device.DeviceFile
-import org.oxycblt.musikr.pipeline.RawSong
+import org.oxycblt.musikr.metadata.Properties
+import org.oxycblt.musikr.tag.parse.ParsedTags
-abstract class Cache {
- internal abstract suspend fun read(file: DeviceFile, covers: Covers): CacheResult
-
- internal abstract suspend fun write(song: RawSong)
-
- internal abstract suspend fun finalize()
-
- abstract class Factory {
- internal abstract fun open(): Cache
- }
+interface Cache {
+ suspend fun read(file: DeviceFile): CacheResult
}
-internal sealed interface CacheResult {
- data class Hit(val song: RawSong) : CacheResult
+interface MutableCache : Cache {
+ suspend fun write(cachedSong: CachedSong)
- data class Miss(val file: DeviceFile, val addedMs: Long?) : CacheResult
+ suspend fun cleanup(excluding: List)
+}
+
+data class CachedSong(
+ val file: DeviceFile,
+ val properties: Properties,
+ val tags: ParsedTags,
+ val coverId: String?,
+ val addedMs: Long
+)
+
+sealed interface CacheResult {
+ data class Hit(val song: CachedSong) : CacheResult
+
+ data class Miss(val file: DeviceFile) : CacheResult
+
+ data class Stale(val file: DeviceFile, val addedMs: Long) : CacheResult
}
diff --git a/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt b/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt
deleted file mode 100644
index 19ba41ab2..000000000
--- a/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt
+++ /dev/null
@@ -1,209 +0,0 @@
-/*
- * Copyright (c) 2023 Auxio Project
- * CacheDatabase.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.cache
-
-import android.content.Context
-import androidx.room.Dao
-import androidx.room.Database
-import androidx.room.Entity
-import androidx.room.Insert
-import androidx.room.OnConflictStrategy
-import androidx.room.PrimaryKey
-import androidx.room.Query
-import androidx.room.Room
-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.fs.device.DeviceFile
-import org.oxycblt.musikr.metadata.Properties
-import org.oxycblt.musikr.pipeline.RawSong
-import org.oxycblt.musikr.tag.Date
-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)
-internal abstract class CacheDatabase : RoomDatabase() {
- abstract fun visibleDao(): VisibleCacheDao
-
- abstract fun invisibleDao(): InvisibleCacheDao
-
- abstract fun writeDao(): CacheWriteDao
-
- companion object {
- fun from(context: Context) =
- Room.databaseBuilder(
- context.applicationContext, CacheDatabase::class.java, "music_cache.db")
- .fallbackToDestructiveMigration()
- .build()
- }
-}
-
-@Dao
-internal interface VisibleCacheDao {
- @Query("SELECT * FROM CachedSong WHERE uri = :uri")
- suspend fun selectSong(uri: String): CachedSong?
-
- @Query("SELECT addedMs FROM CachedSong WHERE uri = :uri")
- suspend fun selectAddedMs(uri: String): Long?
-
- @Transaction suspend fun touch(uri: String) = updateTouchedNs(uri, System.nanoTime())
-
- @Query("UPDATE CachedSong SET touchedNs = :nowNs WHERE uri = :uri")
- suspend fun updateTouchedNs(uri: String, nowNs: Long)
-}
-
-@Dao
-internal interface InvisibleCacheDao {
- @Query("SELECT addedMs FROM CachedSong WHERE uri = :uri")
- suspend fun selectAddedMs(uri: String): Long?
-}
-
-@Dao
-internal interface CacheWriteDao {
- @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun updateSong(cachedSong: CachedSong)
-
- @Query("DELETE FROM CachedSong WHERE touchedNs < :now") suspend fun pruneOlderThan(now: Long)
-}
-
-@Entity
-@TypeConverters(CachedSong.Converters::class)
-internal data class CachedSong(
- @PrimaryKey val uri: String,
- val modifiedMs: Long,
- val addedMs: Long,
- val touchedNs: Long,
- val mimeType: String,
- val durationMs: Long,
- val bitrateHz: Int,
- val sampleRateHz: Int,
- val musicBrainzId: String?,
- val name: String?,
- val sortName: String?,
- val track: Int?,
- val disc: Int?,
- val subtitle: String?,
- val date: Date?,
- val albumMusicBrainzId: String?,
- val albumName: String?,
- val albumSortName: String?,
- val releaseTypes: List,
- val artistMusicBrainzIds: List,
- val artistNames: List,
- val artistSortNames: List,
- val albumArtistMusicBrainzIds: List,
- val albumArtistNames: List,
- val albumArtistSortNames: List,
- val genreNames: List,
- val replayGainTrackAdjustment: Float?,
- val replayGainAlbumAdjustment: Float?,
- val coverId: String?,
-) {
- suspend fun intoRawSong(file: DeviceFile, covers: Covers): RawSong? {
- val cover =
- when (val result = coverId?.let { covers.obtain(it) }) {
- // We found the cover.
- is CoverResult.Hit -> result.cover
- // We actually didn't find the cover, can't safely convert.
- is CoverResult.Miss -> return null
- // No cover in the first place, can ignore.
- null -> null
- }
- return RawSong(
- file,
- Properties(mimeType, durationMs, bitrateHz, sampleRateHz),
- ParsedTags(
- musicBrainzId = musicBrainzId,
- name = name,
- sortName = sortName,
- durationMs = durationMs,
- track = track,
- disc = disc,
- subtitle = subtitle,
- date = date,
- albumMusicBrainzId = albumMusicBrainzId,
- albumName = albumName,
- albumSortName = albumSortName,
- releaseTypes = releaseTypes,
- artistMusicBrainzIds = artistMusicBrainzIds,
- artistNames = artistNames,
- artistSortNames = artistSortNames,
- albumArtistMusicBrainzIds = albumArtistMusicBrainzIds,
- albumArtistNames = albumArtistNames,
- albumArtistSortNames = albumArtistSortNames,
- genreNames = genreNames,
- replayGainTrackAdjustment = replayGainTrackAdjustment,
- replayGainAlbumAdjustment = replayGainAlbumAdjustment),
- cover = cover,
- addedMs = addedMs)
- }
-
- object Converters {
- @TypeConverter
- fun fromMultiValue(values: List) =
- values.joinToString(";") { it.replace(";", "\\;") }
-
- @TypeConverter
- fun toMultiValue(string: String) = string.splitEscaped { it == ';' }.correctWhitespace()
-
- @TypeConverter fun fromDate(date: Date?) = date?.toString()
-
- @TypeConverter fun toDate(string: String?) = string?.let(Date::from)
- }
-
- companion object {
- fun fromRawSong(rawSong: RawSong) =
- CachedSong(
- uri = rawSong.file.uri.toString(),
- modifiedMs = rawSong.file.modifiedMs,
- addedMs = rawSong.addedMs,
- // Should be strictly monotonic so we don't prune this
- // by accident later.
- touchedNs = System.nanoTime(),
- musicBrainzId = rawSong.tags.musicBrainzId,
- name = rawSong.tags.name,
- sortName = rawSong.tags.sortName,
- durationMs = rawSong.tags.durationMs,
- track = rawSong.tags.track,
- disc = rawSong.tags.disc,
- subtitle = rawSong.tags.subtitle,
- date = rawSong.tags.date,
- albumMusicBrainzId = rawSong.tags.albumMusicBrainzId,
- albumName = rawSong.tags.albumName,
- albumSortName = rawSong.tags.albumSortName,
- releaseTypes = rawSong.tags.releaseTypes,
- artistMusicBrainzIds = rawSong.tags.artistMusicBrainzIds,
- artistNames = rawSong.tags.artistNames,
- artistSortNames = rawSong.tags.artistSortNames,
- albumArtistMusicBrainzIds = rawSong.tags.albumArtistMusicBrainzIds,
- albumArtistNames = rawSong.tags.albumArtistNames,
- albumArtistSortNames = rawSong.tags.albumArtistSortNames,
- genreNames = rawSong.tags.genreNames,
- replayGainTrackAdjustment = rawSong.tags.replayGainTrackAdjustment,
- replayGainAlbumAdjustment = rawSong.tags.replayGainAlbumAdjustment,
- coverId = rawSong.cover?.id,
- mimeType = rawSong.properties.mimeType,
- bitrateHz = rawSong.properties.bitrateKbps,
- sampleRateHz = rawSong.properties.sampleRateHz)
- }
-}
diff --git a/musikr/src/main/java/org/oxycblt/musikr/cache/StoredCache.kt b/musikr/src/main/java/org/oxycblt/musikr/cache/StoredCache.kt
deleted file mode 100644
index c4107c3a5..000000000
--- a/musikr/src/main/java/org/oxycblt/musikr/cache/StoredCache.kt
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright (c) 2024 Auxio Project
- * StoredCache.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.cache
-
-import android.content.Context
-import org.oxycblt.musikr.cover.Cover
-import org.oxycblt.musikr.cover.Covers
-import org.oxycblt.musikr.fs.device.DeviceFile
-import org.oxycblt.musikr.pipeline.RawSong
-
-interface StoredCache {
- fun visible(): Cache.Factory
-
- fun invisible(): Cache.Factory
-
- companion object {
- fun from(context: Context): StoredCache = StoredCacheImpl(CacheDatabase.from(context))
- }
-}
-
-private class StoredCacheImpl(private val cacheDatabase: CacheDatabase) : StoredCache {
- override fun visible(): Cache.Factory = VisibleStoredCache.Factory(cacheDatabase)
-
- override fun invisible(): Cache.Factory = InvisibleStoredCache.Factory(cacheDatabase)
-}
-
-private abstract class BaseStoredCache(protected val writeDao: CacheWriteDao) : Cache() {
- private val created = System.nanoTime()
-
- override suspend fun write(song: RawSong) = writeDao.updateSong(CachedSong.fromRawSong(song))
-
- override suspend fun finalize() {
- // Anything not create during this cache's use implies that it has not been
- // access during this run and should be pruned.
- writeDao.pruneOlderThan(created)
- }
-}
-
-private class VisibleStoredCache(private val visibleDao: VisibleCacheDao, writeDao: CacheWriteDao) :
- BaseStoredCache(writeDao) {
- 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.
- // Send back it with the timestamp so it will be re-used.
- // The touch timestamp will be updated on write.
- return CacheResult.Miss(file, song.addedMs)
- }
- // Valid file, update the touch time.
- visibleDao.touch(file.uri.toString())
- val rawSong = song.intoRawSong(file, covers) ?: return CacheResult.Miss(file, song.addedMs)
- return CacheResult.Hit(rawSong)
- }
-
- class Factory(private val cacheDatabase: CacheDatabase) : Cache.Factory() {
- override fun open() =
- VisibleStoredCache(cacheDatabase.visibleDao(), cacheDatabase.writeDao())
- }
-}
-
-private class InvisibleStoredCache(
- private val invisibleCacheDao: InvisibleCacheDao,
- writeDao: CacheWriteDao
-) : BaseStoredCache(writeDao) {
- override suspend fun read(file: DeviceFile, covers: Covers) =
- CacheResult.Miss(file, invisibleCacheDao.selectAddedMs(file.uri.toString()))
-
- class Factory(private val cacheDatabase: CacheDatabase) : Cache.Factory() {
- override fun open() =
- InvisibleStoredCache(cacheDatabase.invisibleDao(), cacheDatabase.writeDao())
- }
-}
diff --git a/musikr/src/main/java/org/oxycblt/musikr/cache/db/CacheDatabase.kt b/musikr/src/main/java/org/oxycblt/musikr/cache/db/CacheDatabase.kt
new file mode 100644
index 000000000..9475556c0
--- /dev/null
+++ b/musikr/src/main/java/org/oxycblt/musikr/cache/db/CacheDatabase.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright (c) 2023 Auxio Project
+ * CacheDatabase.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.cache.db
+
+import android.content.Context
+import android.net.Uri
+import androidx.room.Dao
+import androidx.room.Database
+import androidx.room.Entity
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.PrimaryKey
+import androidx.room.Query
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.Transaction
+import androidx.room.TypeConverter
+import androidx.room.TypeConverters
+import org.oxycblt.musikr.tag.Date
+import org.oxycblt.musikr.util.correctWhitespace
+import org.oxycblt.musikr.util.splitEscaped
+
+@Database(entities = [CachedSongData::class], version = 61, exportSchema = false)
+internal abstract class CacheDatabase : RoomDatabase() {
+ abstract fun readDao(): CacheReadDao
+
+ abstract fun writeDao(): CacheWriteDao
+
+ companion object {
+ fun from(context: Context) =
+ Room.databaseBuilder(
+ context.applicationContext, CacheDatabase::class.java, "music_cache.db")
+ .fallbackToDestructiveMigration()
+ .build()
+ }
+}
+
+@Dao
+internal interface CacheReadDao {
+ @Query("SELECT * FROM CachedSongData WHERE uri = :uri")
+ suspend fun selectSong(uri: String): CachedSongData?
+}
+
+@Dao
+internal interface CacheWriteDao {
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun updateSong(CachedSongData: CachedSongData)
+
+ @Transaction
+ suspend fun deleteExcludingUris(uris: Set) {
+ val delete = selectAllUris().toSet() - uris
+ for (chunk in delete.chunked(999)) {
+ deleteExcludingUriChunk(chunk)
+ }
+ }
+
+ @Query("SELECT uri FROM CachedSongData") suspend fun selectAllUris(): List
+
+ @Query("DELETE FROM CachedSongData WHERE uri IN (:uris)")
+ suspend fun deleteExcludingUriChunk(uris: List)
+}
+
+@Entity
+@TypeConverters(CachedSongData.Converters::class)
+internal data class CachedSongData(
+ @PrimaryKey val uri: Uri,
+ val modifiedMs: Long,
+ val addedMs: Long,
+ val mimeType: String,
+ val durationMs: Long,
+ val bitrateKbps: Int,
+ val sampleRateHz: Int,
+ val musicBrainzId: String?,
+ val name: String?,
+ val sortName: String?,
+ val track: Int?,
+ val disc: Int?,
+ val subtitle: String?,
+ val date: Date?,
+ val albumMusicBrainzId: String?,
+ val albumName: String?,
+ val albumSortName: String?,
+ val releaseTypes: List,
+ val artistMusicBrainzIds: List,
+ val artistNames: List,
+ val artistSortNames: List,
+ val albumArtistMusicBrainzIds: List,
+ val albumArtistNames: List,
+ val albumArtistSortNames: List,
+ val genreNames: List,
+ val replayGainTrackAdjustment: Float?,
+ val replayGainAlbumAdjustment: Float?,
+ val coverId: String?,
+) {
+ object Converters {
+ @TypeConverter
+ fun fromMultiValue(values: List) =
+ values.joinToString(";") { it.replace(";", "\\;") }
+
+ @TypeConverter
+ fun toMultiValue(string: String) = string.splitEscaped { it == ';' }.correctWhitespace()
+
+ @TypeConverter fun fromDate(date: Date?) = date?.toString()
+
+ @TypeConverter fun toDate(string: String?) = string?.let(Date::from)
+
+ @TypeConverter fun toUri(string: String) = Uri.parse(string)
+
+ @TypeConverter fun fromUri(uri: Uri) = uri.toString()
+ }
+}
diff --git a/musikr/src/main/java/org/oxycblt/musikr/cache/db/DBCache.kt b/musikr/src/main/java/org/oxycblt/musikr/cache/db/DBCache.kt
new file mode 100644
index 000000000..310016909
--- /dev/null
+++ b/musikr/src/main/java/org/oxycblt/musikr/cache/db/DBCache.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright (c) 2025 Auxio Project
+ * DBCache.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.cache.db
+
+import android.content.Context
+import org.oxycblt.musikr.cache.Cache
+import org.oxycblt.musikr.cache.CacheResult
+import org.oxycblt.musikr.cache.CachedSong
+import org.oxycblt.musikr.cache.MutableCache
+import org.oxycblt.musikr.fs.device.DeviceFile
+import org.oxycblt.musikr.metadata.Properties
+import org.oxycblt.musikr.tag.parse.ParsedTags
+
+open class DBCache internal constructor(private val readDao: CacheReadDao) : Cache {
+ override suspend fun read(file: DeviceFile): CacheResult {
+ val dbSong = readDao.selectSong(file.uri.toString()) ?: return CacheResult.Miss(file)
+ if (dbSong.modifiedMs != file.modifiedMs) {
+ return CacheResult.Stale(file, dbSong.addedMs)
+ }
+ val song =
+ CachedSong(
+ file,
+ Properties(
+ dbSong.mimeType, dbSong.durationMs, dbSong.bitrateKbps, dbSong.sampleRateHz),
+ ParsedTags(
+ musicBrainzId = dbSong.musicBrainzId,
+ name = dbSong.name,
+ sortName = dbSong.sortName,
+ durationMs = dbSong.durationMs,
+ track = dbSong.track,
+ disc = dbSong.disc,
+ subtitle = dbSong.subtitle,
+ date = dbSong.date,
+ albumMusicBrainzId = dbSong.albumMusicBrainzId,
+ albumName = dbSong.albumName,
+ albumSortName = dbSong.albumSortName,
+ releaseTypes = dbSong.releaseTypes,
+ artistMusicBrainzIds = dbSong.artistMusicBrainzIds,
+ artistNames = dbSong.artistNames,
+ artistSortNames = dbSong.artistSortNames,
+ albumArtistMusicBrainzIds = dbSong.albumArtistMusicBrainzIds,
+ albumArtistNames = dbSong.albumArtistNames,
+ albumArtistSortNames = dbSong.albumArtistSortNames,
+ genreNames = dbSong.genreNames,
+ replayGainTrackAdjustment = dbSong.replayGainTrackAdjustment,
+ replayGainAlbumAdjustment = dbSong.replayGainAlbumAdjustment),
+ coverId = dbSong.coverId,
+ addedMs = dbSong.addedMs)
+ return CacheResult.Hit(song)
+ }
+
+ companion object {
+ fun from(context: Context) = DBCache(CacheDatabase.from(context).readDao())
+ }
+}
+
+class MutableDBCache
+private constructor(readDao: CacheReadDao, private val writeDao: CacheWriteDao) :
+ MutableCache, DBCache(readDao) {
+ override suspend fun write(cachedSong: CachedSong) {
+ val dbSong =
+ CachedSongData(
+ uri = cachedSong.file.uri,
+ modifiedMs = cachedSong.file.modifiedMs,
+ addedMs = cachedSong.addedMs,
+ mimeType = cachedSong.properties.mimeType,
+ durationMs = cachedSong.properties.durationMs,
+ bitrateKbps = cachedSong.properties.bitrateKbps,
+ sampleRateHz = cachedSong.properties.sampleRateHz,
+ musicBrainzId = cachedSong.tags.musicBrainzId,
+ name = cachedSong.tags.name,
+ sortName = cachedSong.tags.sortName,
+ track = cachedSong.tags.track,
+ disc = cachedSong.tags.disc,
+ subtitle = cachedSong.tags.subtitle,
+ date = cachedSong.tags.date,
+ albumMusicBrainzId = cachedSong.tags.albumMusicBrainzId,
+ albumName = cachedSong.tags.albumName,
+ albumSortName = cachedSong.tags.albumSortName,
+ releaseTypes = cachedSong.tags.releaseTypes,
+ artistMusicBrainzIds = cachedSong.tags.artistMusicBrainzIds,
+ artistNames = cachedSong.tags.artistNames,
+ artistSortNames = cachedSong.tags.artistSortNames,
+ albumArtistMusicBrainzIds = cachedSong.tags.albumArtistMusicBrainzIds,
+ albumArtistNames = cachedSong.tags.albumArtistNames,
+ albumArtistSortNames = cachedSong.tags.albumArtistSortNames,
+ genreNames = cachedSong.tags.genreNames,
+ replayGainTrackAdjustment = cachedSong.tags.replayGainTrackAdjustment,
+ replayGainAlbumAdjustment = cachedSong.tags.replayGainAlbumAdjustment,
+ coverId = cachedSong.coverId)
+ writeDao.updateSong(dbSong)
+ }
+
+ override suspend fun cleanup(excluding: List) {
+ writeDao.deleteExcludingUris(excluding.mapTo(mutableSetOf()) { it.file.uri.toString() })
+ }
+
+ companion object {
+ fun from(context: Context): MutableDBCache {
+ val db = CacheDatabase.from(context)
+ return MutableDBCache(db.readDao(), db.writeDao())
+ }
+ }
+}
diff --git a/musikr/src/main/java/org/oxycblt/musikr/cover/Covers.kt b/musikr/src/main/java/org/oxycblt/musikr/covers/Covers.kt
similarity index 96%
rename from musikr/src/main/java/org/oxycblt/musikr/cover/Covers.kt
rename to musikr/src/main/java/org/oxycblt/musikr/covers/Covers.kt
index 902bce5c5..adef4cf44 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/cover/Covers.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/covers/Covers.kt
@@ -16,8 +16,9 @@
* along with this program. If not, see .
*/
-package org.oxycblt.musikr.cover
+package org.oxycblt.musikr.covers
+import android.os.ParcelFileDescriptor
import java.io.InputStream
import org.oxycblt.musikr.fs.device.DeviceFile
import org.oxycblt.musikr.metadata.Metadata
@@ -94,6 +95,10 @@ interface Cover {
override fun hashCode(): Int
}
+interface FDCover : Cover {
+ suspend fun fd(): ParcelFileDescriptor?
+}
+
class CoverCollection private constructor(val covers: List) {
override fun hashCode() = covers.hashCode()
diff --git a/musikr/src/main/java/org/oxycblt/musikr/cover/CoverFormat.kt b/musikr/src/main/java/org/oxycblt/musikr/covers/embedded/CoverFormat.kt
similarity index 97%
rename from musikr/src/main/java/org/oxycblt/musikr/cover/CoverFormat.kt
rename to musikr/src/main/java/org/oxycblt/musikr/covers/embedded/CoverFormat.kt
index 16af47506..6ed9bce02 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/cover/CoverFormat.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/covers/embedded/CoverFormat.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024 Auxio Project
+ * Copyright (c) 2025 Auxio Project
* CoverFormat.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-package org.oxycblt.musikr.cover
+package org.oxycblt.musikr.covers.embedded
import android.graphics.Bitmap
import android.graphics.BitmapFactory
diff --git a/musikr/src/main/java/org/oxycblt/musikr/cover/CoverIdentifier.kt b/musikr/src/main/java/org/oxycblt/musikr/covers/embedded/CoverIdentifier.kt
similarity index 96%
rename from musikr/src/main/java/org/oxycblt/musikr/cover/CoverIdentifier.kt
rename to musikr/src/main/java/org/oxycblt/musikr/covers/embedded/CoverIdentifier.kt
index ef0917e05..d842e813d 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/cover/CoverIdentifier.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/covers/embedded/CoverIdentifier.kt
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-package org.oxycblt.musikr.cover
+package org.oxycblt.musikr.covers.embedded
import java.security.MessageDigest
diff --git a/musikr/src/main/java/org/oxycblt/musikr/cover/CoverParams.kt b/musikr/src/main/java/org/oxycblt/musikr/covers/embedded/CoverParams.kt
similarity index 96%
rename from musikr/src/main/java/org/oxycblt/musikr/cover/CoverParams.kt
rename to musikr/src/main/java/org/oxycblt/musikr/covers/embedded/CoverParams.kt
index 1b26dc63f..e4ae6e8cf 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/cover/CoverParams.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/covers/embedded/CoverParams.kt
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-package org.oxycblt.musikr.cover
+package org.oxycblt.musikr.covers.embedded
class CoverParams private constructor(val resolution: Int, val quality: Int) {
override fun hashCode() = 31 * resolution + quality
diff --git a/musikr/src/main/java/org/oxycblt/musikr/cover/FileCovers.kt b/musikr/src/main/java/org/oxycblt/musikr/covers/embedded/EmbeddedCovers.kt
similarity index 58%
rename from musikr/src/main/java/org/oxycblt/musikr/cover/FileCovers.kt
rename to musikr/src/main/java/org/oxycblt/musikr/covers/embedded/EmbeddedCovers.kt
index aadafad1d..33fec914f 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/cover/FileCovers.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/covers/embedded/EmbeddedCovers.kt
@@ -1,6 +1,6 @@
/*
* Copyright (c) 2025 Auxio Project
- * FileCovers.kt is part of Auxio.
+ * InternalCovers.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
@@ -16,20 +16,24 @@
* along with this program. If not, see .
*/
-package org.oxycblt.musikr.cover
+package org.oxycblt.musikr.covers.embedded
-import android.os.ParcelFileDescriptor
+import org.oxycblt.musikr.covers.Cover
+import org.oxycblt.musikr.covers.CoverResult
+import org.oxycblt.musikr.covers.Covers
+import org.oxycblt.musikr.covers.FDCover
+import org.oxycblt.musikr.covers.MutableCovers
+import org.oxycblt.musikr.fs.app.AppFS
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): CoverResult {
- val file = appFiles.find(getFileName(id))
+open class EmbeddedCovers(private val appFS: AppFS, private val coverFormat: CoverFormat) :
+ Covers {
+ override suspend fun obtain(id: String): CoverResult {
+ val file = appFS.find(getFileName(id))
return if (file != null) {
- CoverResult.Hit(FileCoverImpl(id, file))
+ CoverResult.Hit(InternalCoverImpl(id, file))
} else {
CoverResult.Miss()
}
@@ -38,30 +42,26 @@ open class FileCovers(private val appFiles: AppFiles, private val coverFormat: C
protected fun getFileName(id: String) = "$id.${coverFormat.extension}"
}
-class MutableFileCovers(
- private val appFiles: AppFiles,
+class MutableEmbeddedCovers(
+ private val appFS: AppFS,
private val coverFormat: CoverFormat,
private val coverIdentifier: CoverIdentifier
-) : FileCovers(appFiles, coverFormat), MutableCovers {
- override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult {
+) : EmbeddedCovers(appFS, coverFormat), MutableCovers {
+ override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult {
val data = metadata.cover ?: return CoverResult.Miss()
val id = coverIdentifier.identify(data)
- val coverFile = appFiles.write(getFileName(id)) { coverFormat.transcodeInto(data, it) }
- return CoverResult.Hit(FileCoverImpl(id, coverFile))
+ val coverFile = appFS.write(getFileName(id)) { coverFormat.transcodeInto(data, it) }
+ return CoverResult.Hit(InternalCoverImpl(id, coverFile))
}
override suspend fun cleanup(excluding: Collection) {
val used = excluding.mapTo(mutableSetOf()) { getFileName(it.id) }
- appFiles.deleteWhere { it !in used }
+ appFS.deleteWhere { it !in used }
}
}
-interface FileCover : Cover {
- suspend fun fd(): ParcelFileDescriptor?
-}
-
-private data class FileCoverImpl(override val id: String, private val appFile: AppFile) :
- FileCover {
+private data class InternalCoverImpl(override val id: String, private val appFile: AppFile) :
+ FDCover {
override suspend fun fd() = appFile.fd()
override suspend fun open() = appFile.open()
diff --git a/musikr/src/main/java/org/oxycblt/musikr/cover/FolderCovers.kt b/musikr/src/main/java/org/oxycblt/musikr/covers/fs/FSCovers.kt
similarity index 77%
rename from musikr/src/main/java/org/oxycblt/musikr/cover/FolderCovers.kt
rename to musikr/src/main/java/org/oxycblt/musikr/covers/fs/FSCovers.kt
index ad5383482..b182c775e 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/cover/FolderCovers.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/covers/fs/FSCovers.kt
@@ -1,6 +1,6 @@
/*
* Copyright (c) 2025 Auxio Project
- * FolderCovers.kt is part of Auxio.
+ * FSCovers.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
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-package org.oxycblt.musikr.cover
+package org.oxycblt.musikr.covers.fs
import android.content.Context
import android.net.Uri
@@ -26,22 +26,24 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.withContext
+import org.oxycblt.musikr.covers.Cover
+import org.oxycblt.musikr.covers.CoverResult
+import org.oxycblt.musikr.covers.Covers
+import org.oxycblt.musikr.covers.FDCover
+import org.oxycblt.musikr.covers.MutableCovers
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 {
+open class FSCovers(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 {
@@ -60,10 +62,9 @@ open class FolderCovers(private val context: Context) : Covers {
}
}
-class MutableFolderCovers(private val context: Context) :
- FolderCovers(context), MutableCovers {
- override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult {
- val parent = file.parent
+class MutableFSCovers(private val context: Context) : FSCovers(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))
}
@@ -73,22 +74,23 @@ class MutableFolderCovers(private val context: Context) :
// 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 findCoverInDirectory(directory: DeviceDirectory): DeviceFile? {
+ return directory.children.firstNotNullOfOrNull { node ->
+ if (node is DeviceFile && isCoverArtFile(
+ node
+ )
+ ) node else null
+ }
}
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",
@@ -99,10 +101,8 @@ class MutableFolderCovers(private val context: Context) :
"artwork",
"art",
"folder",
- "cover")
+ "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(".", "")
@@ -115,12 +115,10 @@ class MutableFolderCovers(private val context: Context) :
}
}
-interface FolderCover : FileCover
-
private data class FolderCoverImpl(
private val context: Context,
private val uri: Uri,
-) : FolderCover {
+) : FDCover {
override val id = "folder:$uri"
override suspend fun fd(): ParcelFileDescriptor? =
diff --git a/musikr/src/main/java/org/oxycblt/musikr/fs/app/AppFiles.kt b/musikr/src/main/java/org/oxycblt/musikr/fs/app/AppFS.kt
similarity index 94%
rename from musikr/src/main/java/org/oxycblt/musikr/fs/app/AppFiles.kt
rename to musikr/src/main/java/org/oxycblt/musikr/fs/app/AppFS.kt
index 9c8f2a407..50b423c08 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/fs/app/AppFiles.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/fs/app/AppFS.kt
@@ -1,6 +1,6 @@
/*
* Copyright (c) 2024 Auxio Project
- * AppFiles.kt is part of Auxio.
+ * AppFS.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
@@ -28,7 +28,7 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
-interface AppFiles {
+interface AppFS {
suspend fun find(name: String): AppFile?
suspend fun write(name: String, block: suspend (OutputStream) -> Unit): AppFile
@@ -36,9 +36,9 @@ interface AppFiles {
suspend fun deleteWhere(block: (String) -> Boolean)
companion object {
- suspend fun at(dir: File): AppFiles {
+ suspend fun at(dir: File): AppFS {
withContext(Dispatchers.IO) { check(dir.exists() && dir.isDirectory) }
- return AppFilesImpl(dir)
+ return AppFSImpl(dir)
}
}
}
@@ -49,7 +49,7 @@ interface AppFile {
suspend fun open(): InputStream?
}
-private class AppFilesImpl(private val dir: File) : AppFiles {
+private class AppFSImpl(private val dir: File) : AppFS {
private val fileMutexes = mutableMapOf()
private val mapMutex = Mutex()
diff --git a/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFS.kt b/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFS.kt
new file mode 100644
index 000000000..bb580008c
--- /dev/null
+++ b/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFS.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright (c) 2024 Auxio Project
+ * DeviceFS.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.fs.device
+
+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
+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.MusicLocation
+import org.oxycblt.musikr.fs.Path
+
+internal interface DeviceFS {
+ fun explore(locations: Flow): Flow
+
+ companion object {
+ fun from(context: Context, withHidden: Boolean): DeviceFS =
+ DeviceFSImpl(context.contentResolverSafe, withHidden)
+ }
+}
+
+sealed interface DeviceNode {
+ val uri: Uri
+ val path: Path
+}
+
+data class DeviceDirectory(
+ override val uri: Uri,
+ override val path: Path,
+ val parent: Deferred?,
+ var children: List
+) : DeviceNode
+
+data class DeviceFile(
+ override val uri: Uri,
+ override val path: Path,
+ val modifiedMs: Long,
+ val mimeType: String,
+ val size: Long,
+ val parent: Deferred
+) : DeviceNode
+
+@OptIn(ExperimentalCoroutinesApi::class)
+private class DeviceFSImpl(
+ private val contentResolver: ContentResolver,
+ private val withHidden: Boolean
+) : DeviceFS {
+ override fun explore(locations: Flow): Flow =
+ locations.flatMapMerge { location ->
+ exploreDirectoryImpl(
+ location.uri,
+ DocumentsContract.getTreeDocumentId(location.uri),
+ location.path,
+ null
+ )
+ }
+
+ private fun exploreDirectoryImpl(
+ rootUri: Uri,
+ treeDocumentId: String,
+ relativePath: Path,
+ parent: Deferred?
+ ): Flow = flow {
+ // 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 (!withHidden && 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)
+ }
+ }
+ directoryDeferred.complete(DeviceDirectory(uri, relativePath, parent, children))
+ emitAll(recursive.asFlow().flattenMerge())
+ }
+ }
+
+ private companion object {
+ val PROJECTION =
+ arrayOf(
+ DocumentsContract.Document.COLUMN_DOCUMENT_ID,
+ DocumentsContract.Document.COLUMN_DISPLAY_NAME,
+ DocumentsContract.Document.COLUMN_MIME_TYPE,
+ DocumentsContract.Document.COLUMN_SIZE,
+ DocumentsContract.Document.COLUMN_LAST_MODIFIED
+ )
+ }
+}
diff --git a/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFile.kt b/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFile.kt
deleted file mode 100644
index 6590491a6..000000000
--- a/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFile.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright (c) 2024 Auxio Project
- * DeviceFile.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.fs.device
-
-import android.net.Uri
-import kotlinx.coroutines.flow.Flow
-import org.oxycblt.musikr.fs.Path
-
-sealed interface DeviceNode {
- val uri: Uri
- val path: Path
-}
-
-data class DeviceDirectory(
- override val uri: Uri,
- override val path: Path,
- val parent: DeviceDirectory?,
- var children: Flow
-) : DeviceNode
-
-data class DeviceFile(
- override val uri: Uri,
- override val path: Path,
- val modifiedMs: Long,
- val mimeType: String,
- val size: Long,
- val parent: DeviceDirectory
-) : 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
deleted file mode 100644
index 2ba57558d..000000000
--- a/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFiles.kt
+++ /dev/null
@@ -1,139 +0,0 @@
-/*
- * Copyright (c) 2024 Auxio Project
- * DeviceFiles.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.fs.device
-
-import android.content.ContentResolver
-import android.content.Context
-import android.net.Uri
-import android.provider.DocumentsContract
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.emptyFlow
-import kotlinx.coroutines.flow.flatMapMerge
-import kotlinx.coroutines.flow.flow
-import org.oxycblt.musikr.fs.MusicLocation
-import org.oxycblt.musikr.fs.Path
-
-internal interface DeviceFiles {
- fun explore(locations: Flow, ignoreHidden: Boolean = true): Flow
-
- companion object {
- fun from(context: Context): DeviceFiles = DeviceFilesImpl(context.contentResolverSafe)
- }
-}
-
-@OptIn(ExperimentalCoroutinesApi::class)
-private class DeviceFilesImpl(private val contentResolver: ContentResolver) : DeviceFiles {
- override fun explore(locations: Flow, ignoreHidden: Boolean): Flow =
- locations.flatMapMerge { location ->
- // Create a root directory for each location
- val rootDirectory =
- DeviceDirectory(
- uri = location.uri, path = location.path, parent = null, children = emptyFlow())
-
- // Set up the children flow for the root directory
- rootDirectory.children =
- exploreDirectoryImpl(
- contentResolver,
- location.uri,
- DocumentsContract.getTreeDocumentId(location.uri),
- location.path,
- rootDirectory,
- ignoreHidden)
-
- // Return a flow that emits the root directory
- flow { emit(rootDirectory) }
- }
-
- private fun exploreDirectoryImpl(
- contentResolver: ContentResolver,
- rootUri: Uri,
- treeDocumentId: String,
- relativePath: Path,
- parent: DeviceDirectory,
- ignoreHidden: Boolean
- ): 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)
-
- 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)
- val childUri = DocumentsContract.buildDocumentUriUsingTree(rootUri, childId)
-
- if (mimeType == DocumentsContract.Document.MIME_TYPE_DIR) {
- // Create a directory node with empty children flow initially
- val directory =
- DeviceDirectory(
- uri = childUri,
- path = newPath,
- parent = parent,
- children = emptyFlow())
-
- // Set up the children flow for this directory
- directory.children =
- exploreDirectoryImpl(
- contentResolver, rootUri, childId, newPath, directory, ignoreHidden)
-
- // Emit the directory node
- emit(directory)
- } else {
- val size = cursor.getLong(sizeIndex)
- emit(
- DeviceFile(
- uri = childUri,
- mimeType = mimeType,
- path = newPath,
- size = size,
- modifiedMs = lastModified,
- parent = parent))
- }
- }
- }
- }
-
- private companion object {
- val PROJECTION =
- arrayOf(
- DocumentsContract.Document.COLUMN_DOCUMENT_ID,
- DocumentsContract.Document.COLUMN_DISPLAY_NAME,
- DocumentsContract.Document.COLUMN_MIME_TYPE,
- DocumentsContract.Document.COLUMN_SIZE,
- DocumentsContract.Document.COLUMN_LAST_MODIFIED)
- }
-}
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 7ce64d949..b9f57c16f 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/metadata/MetadataExtractor.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/metadata/MetadataExtractor.kt
@@ -18,24 +18,29 @@
package org.oxycblt.musikr.metadata
-import android.os.ParcelFileDescriptor
+import android.content.ContentResolver
+import android.content.Context
import java.io.FileInputStream
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.oxycblt.musikr.fs.device.DeviceFile
internal interface MetadataExtractor {
- suspend fun extract(deviceFile: DeviceFile, fd: ParcelFileDescriptor): Metadata?
+ suspend fun extract(deviceFile: DeviceFile): Metadata?
companion object {
- fun new(): MetadataExtractor = MetadataExtractorImpl
+ fun from(context: Context): MetadataExtractor =
+ MetadataExtractorImpl(context.contentResolver)
}
}
-private object MetadataExtractorImpl : MetadataExtractor {
- override suspend fun extract(deviceFile: DeviceFile, fd: ParcelFileDescriptor) =
+private class MetadataExtractorImpl(private val contentResolver: ContentResolver) :
+ MetadataExtractor {
+ override suspend fun extract(deviceFile: DeviceFile): Metadata? =
withContext(Dispatchers.IO) {
- val fis = FileInputStream(fd.fileDescriptor)
- TagLibJNI.open(deviceFile, fis).also { fis.close() }
+ contentResolver.openFileDescriptor(deviceFile.uri, "r")?.use { fd ->
+ val fis = FileInputStream(fd.fileDescriptor)
+ TagLibJNI.open(deviceFile, fis).also { fis.close() }
+ }
}
}
diff --git a/musikr/src/main/java/org/oxycblt/musikr/model/AlbumImpl.kt b/musikr/src/main/java/org/oxycblt/musikr/model/AlbumImpl.kt
index 6031569b8..c4225d9f4 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/model/AlbumImpl.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/model/AlbumImpl.kt
@@ -22,7 +22,7 @@ import org.oxycblt.musikr.Album
import org.oxycblt.musikr.Artist
import org.oxycblt.musikr.Music
import org.oxycblt.musikr.Song
-import org.oxycblt.musikr.cover.CoverCollection
+import org.oxycblt.musikr.covers.CoverCollection
import org.oxycblt.musikr.tag.Date
import org.oxycblt.musikr.tag.interpret.PreAlbum
import org.oxycblt.musikr.util.update
diff --git a/musikr/src/main/java/org/oxycblt/musikr/model/ArtistImpl.kt b/musikr/src/main/java/org/oxycblt/musikr/model/ArtistImpl.kt
index e05740401..e2820a0bb 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/model/ArtistImpl.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/model/ArtistImpl.kt
@@ -23,7 +23,7 @@ import org.oxycblt.musikr.Artist
import org.oxycblt.musikr.Genre
import org.oxycblt.musikr.Music
import org.oxycblt.musikr.Song
-import org.oxycblt.musikr.cover.CoverCollection
+import org.oxycblt.musikr.covers.CoverCollection
import org.oxycblt.musikr.tag.interpret.PreArtist
import org.oxycblt.musikr.util.update
diff --git a/musikr/src/main/java/org/oxycblt/musikr/model/GenreImpl.kt b/musikr/src/main/java/org/oxycblt/musikr/model/GenreImpl.kt
index 0805f284b..18d7858d3 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/model/GenreImpl.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/model/GenreImpl.kt
@@ -22,7 +22,7 @@ import org.oxycblt.musikr.Artist
import org.oxycblt.musikr.Genre
import org.oxycblt.musikr.Music
import org.oxycblt.musikr.Song
-import org.oxycblt.musikr.cover.CoverCollection
+import org.oxycblt.musikr.covers.CoverCollection
import org.oxycblt.musikr.tag.interpret.PreGenre
import org.oxycblt.musikr.util.update
diff --git a/musikr/src/main/java/org/oxycblt/musikr/model/PlaylistImpl.kt b/musikr/src/main/java/org/oxycblt/musikr/model/PlaylistImpl.kt
index a92197df5..1837f9967 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/model/PlaylistImpl.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/model/PlaylistImpl.kt
@@ -20,7 +20,7 @@ package org.oxycblt.musikr.model
import org.oxycblt.musikr.Playlist
import org.oxycblt.musikr.Song
-import org.oxycblt.musikr.cover.CoverCollection
+import org.oxycblt.musikr.covers.CoverCollection
import org.oxycblt.musikr.playlist.interpret.PrePlaylistInfo
import org.oxycblt.musikr.tag.Name
diff --git a/musikr/src/main/java/org/oxycblt/musikr/pipeline/EvaluateStep.kt b/musikr/src/main/java/org/oxycblt/musikr/pipeline/EvaluateStep.kt
index df4f72cb1..a28fdab8c 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/EvaluateStep.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/EvaluateStep.kt
@@ -18,16 +18,9 @@
package org.oxycblt.musikr.pipeline
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.buffer
-import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.filterIsInstance
-import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.merge
-import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.fold
import org.oxycblt.musikr.Interpretation
import org.oxycblt.musikr.MutableLibrary
import org.oxycblt.musikr.Storage
@@ -38,7 +31,7 @@ import org.oxycblt.musikr.playlist.interpret.PlaylistInterpreter
import org.oxycblt.musikr.tag.interpret.TagInterpreter
internal interface EvaluateStep {
- suspend fun evaluate(extractedMusic: Flow): MutableLibrary
+ suspend fun evaluate(extractedMusic: Flow): MutableLibrary
companion object {
fun new(storage: Storage, interpretation: Interpretation): EvaluateStep =
@@ -56,33 +49,16 @@ private class EvaluateStepImpl(
private val storedPlaylists: StoredPlaylists,
private val libraryFactory: LibraryFactory
) : EvaluateStep {
- override suspend fun evaluate(extractedMusic: Flow): MutableLibrary {
- val filterFlow =
- extractedMusic.filterIsInstance().divert {
- when (it) {
- is ExtractedMusic.Valid.Song -> Divert.Right(it.song)
- is ExtractedMusic.Valid.Playlist -> Divert.Left(it.file)
+ override suspend fun evaluate(extractedMusic: Flow): MutableLibrary =
+ extractedMusic
+ .filterIsInstance()
+ .tryFold(MusicGraph.builder()) { graphBuilder, extracted ->
+ when (extracted) {
+ is RawSong -> graphBuilder.add(tagInterpreter.interpret(extracted))
+ is RawPlaylist ->
+ graphBuilder.add(playlistInterpreter.interpret(extracted.file))
}
+ graphBuilder
}
- val rawSongs = filterFlow.right
- val preSongs =
- rawSongs
- .map { wrap(it, tagInterpreter::interpret) }
- .flowOn(Dispatchers.Default)
- .buffer(Channel.UNLIMITED)
- val prePlaylists =
- filterFlow.left
- .map { wrap(it, playlistInterpreter::interpret) }
- .flowOn(Dispatchers.Default)
- .buffer(Channel.UNLIMITED)
- val graphBuilder = MusicGraph.builder()
- val graphBuild =
- merge(
- filterFlow.manager,
- preSongs.onEach { wrap(it, graphBuilder::add) },
- prePlaylists.onEach { wrap(it, graphBuilder::add) })
- graphBuild.collect()
- val graph = graphBuilder.build()
- return libraryFactory.create(graph, storedPlaylists, playlistInterpreter)
- }
+ .let { libraryFactory.create(it.build(), storedPlaylists, playlistInterpreter) }
}
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 65f742688..5a7779c13 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt
@@ -25,66 +25,82 @@ 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.flattenMerge
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
+import org.oxycblt.musikr.Interpretation
import org.oxycblt.musikr.Storage
+import org.oxycblt.musikr.cache.Cache
+import org.oxycblt.musikr.cache.CacheResult
+import org.oxycblt.musikr.covers.Cover
+import org.oxycblt.musikr.covers.CoverResult
+import org.oxycblt.musikr.covers.Covers
import org.oxycblt.musikr.fs.MusicLocation
-import org.oxycblt.musikr.fs.device.DeviceDirectory
-import org.oxycblt.musikr.fs.device.DeviceFile
-import org.oxycblt.musikr.fs.device.DeviceFiles
-import org.oxycblt.musikr.fs.device.DeviceNode
-import org.oxycblt.musikr.playlist.PlaylistFile
+import org.oxycblt.musikr.fs.device.DeviceFS
import org.oxycblt.musikr.playlist.db.StoredPlaylists
import org.oxycblt.musikr.playlist.m3u.M3U
internal interface ExploreStep {
- fun explore(locations: List): Flow
+ 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(
+ DeviceFS.from(context, interpretation.withHidden),
+ storage.cache,
+ storage.covers,
+ storage.storedPlaylists)
}
}
private class ExploreStepImpl(
- private val deviceFiles: DeviceFiles,
+ private val deviceFS: DeviceFS,
+ private val cache: Cache,
+ private val covers: Covers,
private val storedPlaylists: StoredPlaylists
) : ExploreStep {
- override fun explore(locations: List): Flow {
- val audios =
- deviceFiles
- .explore(locations.asFlow())
- .flattenFilter { it.mimeType.startsWith("audio/") || it.mimeType == M3U.MIME_TYPE }
- .flowOn(Dispatchers.IO)
- .buffer()
- val playlists =
- flow { emitAll(storedPlaylists.read().asFlow()) }
- .map { ExploreNode.Playlist(it) }
- .flowOn(Dispatchers.IO)
- .buffer()
- return merge(audios, playlists)
- }
-
@OptIn(ExperimentalCoroutinesApi::class)
- private fun Flow.flattenFilter(block: (DeviceFile) -> Boolean): Flow =
- flow {
- collect {
- val recurse = mutableListOf>()
- when {
- it is DeviceFile && block(it) -> emit(ExploreNode.Audio(it))
- it is DeviceDirectory -> recurse.add(it.children.flattenFilter(block))
- else -> {}
+ override fun explore(locations: List): Flow {
+ val addingMs = System.currentTimeMillis()
+ return merge(
+ deviceFS
+ .explore(locations.asFlow(),)
+ .filter { it.mimeType.startsWith("audio/") || it.mimeType == M3U.MIME_TYPE }
+ .distribute(8)
+ .distributedMap { file ->
+ val cachedSong =
+ when (val cacheResult = cache.read(file)) {
+ is CacheResult.Hit -> cacheResult.song
+ is CacheResult.Stale ->
+ return@distributedMap NewSong(cacheResult.file, cacheResult.addedMs)
+ is CacheResult.Miss ->
+ return@distributedMap NewSong(cacheResult.file, addingMs)
+ }
+ val cover =
+ cachedSong.coverId?.let { coverId ->
+ when (val coverResult = covers.obtain(coverId)) {
+ is CoverResult.Hit -> coverResult.cover
+ else ->
+ return@distributedMap NewSong(
+ cachedSong.file, cachedSong.addedMs)
+ }
+ }
+ RawSong(
+ cachedSong.file,
+ cachedSong.properties,
+ cachedSong.tags,
+ cover,
+ cachedSong.addedMs)
}
- emitAll(recurse.asFlow().flattenMerge())
- }
- }
-}
-
-internal sealed interface ExploreNode {
- data class Audio(val file: DeviceFile) : ExploreNode
-
- data class Playlist(val file: PlaylistFile) : ExploreNode
+ .flattenMerge()
+ .flowOn(Dispatchers.IO)
+ .buffer(),
+ flow { emitAll(storedPlaylists.read().asFlow()) }
+ .map { RawPlaylist(it) }
+ .flowOn(Dispatchers.IO)
+ .buffer())
+ }
}
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 576980b3a..c6d347577 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt
@@ -19,181 +19,63 @@
package org.oxycblt.musikr.pipeline
import android.content.Context
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.flattenMerge
-import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.mapNotNull
-import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onCompletion
-import kotlinx.coroutines.withContext
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.device.DeviceFile
+import org.oxycblt.musikr.cache.CachedSong
+import org.oxycblt.musikr.cache.MutableCache
+import org.oxycblt.musikr.covers.Cover
+import org.oxycblt.musikr.covers.CoverResult
+import org.oxycblt.musikr.covers.MutableCovers
import org.oxycblt.musikr.metadata.MetadataExtractor
-import org.oxycblt.musikr.metadata.Properties
-import org.oxycblt.musikr.playlist.PlaylistFile
-import org.oxycblt.musikr.tag.parse.ParsedTags
import org.oxycblt.musikr.tag.parse.TagParser
internal interface ExtractStep {
- fun extract(nodes: Flow): Flow
+ fun extract(nodes: Flow): Flow
companion object {
fun from(context: Context, storage: Storage): ExtractStep =
ExtractStepImpl(
- context,
- MetadataExtractor.new(),
- TagParser.new(),
- storage.cache,
- storage.storedCovers)
+ MetadataExtractor.from(context), TagParser.new(), storage.cache, storage.covers)
}
}
private class ExtractStepImpl(
- private val context: Context,
private val metadataExtractor: MetadataExtractor,
private val tagParser: TagParser,
- private val cacheFactory: Cache.Factory,
+ private val cache: MutableCache,
private val covers: MutableCovers
) : ExtractStep {
@OptIn(ExperimentalCoroutinesApi::class)
- override fun extract(nodes: Flow): Flow {
- val cache = cacheFactory.open()
- val addingMs = System.currentTimeMillis()
- val filterFlow =
- nodes.divert {
+ override fun extract(nodes: Flow): Flow {
+ val exclude = mutableListOf()
+ return nodes
+ .distribute(8)
+ .distributedMap {
when (it) {
- is ExploreNode.Audio -> Divert.Right(it.file)
- is ExploreNode.Playlist -> Divert.Left(it.file)
- }
- }
- 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, covers) } }
- .flowOn(Dispatchers.IO)
- .buffer(Channel.UNLIMITED)
- }
- .flattenMerge()
- .buffer(Channel.UNLIMITED)
-
- // Divert cache hits and misses
- val cacheFlow =
- cacheResults.divert {
- when (it) {
- is CacheResult.Hit -> Divert.Left(it.song)
- 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) }
-
- // Process uncached files in parallel
- val uncachedFiles = cacheFlow.right
- val processingDistributedFlow = uncachedFiles.distribute(8)
-
- // Process each uncached file in parallel flows
- val processedSongs =
- processingDistributedFlow.flows
- .map { flow ->
- flow
- .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
- }
- }
+ is RawSong -> it
+ is RawPlaylist -> it
+ is NewSong -> {
+ val metadata =
+ metadataExtractor.extract(it.file) ?: return@distributedMap InvalidSong
+ val tags = tagParser.parse(metadata)
+ val cover =
+ when (val result = covers.create(it.file, metadata)) {
+ is CoverResult.Hit -> result.cover
+ else -> null
}
- }
- .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 cachedSong =
+ CachedSong(it.file, metadata.properties, tags, cover?.id, it.addedMs)
+ cache.write(cachedSong)
+ exclude.add(cachedSong)
+ val rawSong = RawSong(it.file, metadata.properties, tags, cover, it.addedMs)
+ rawSong
+ }
}
}
-
- val processedValidSongs = processedFlow.left
- val invalidSongs = processedFlow.right
-
- val merged =
- merge(
- filterFlow.manager,
- readDistributedFlow.manager,
- cacheFlow.manager,
- processingDistributedFlow.manager,
- processedFlow.manager,
- cachedSongs,
- processedValidSongs,
- invalidSongs,
- playlistNodes)
-
- return merged.onCompletion { cache.finalize() }
+ .flattenMerge()
+ .onCompletion { cache.cleanup(exclude) }
}
}
-
-internal data class RawSong(
- val file: DeviceFile,
- val properties: Properties,
- val tags: ParsedTags,
- val cover: Cover?,
- val addedMs: Long
-)
-
-internal sealed interface ExtractedMusic {
- sealed interface Valid : ExtractedMusic {
- data class Song(val song: RawSong) : Valid
-
- data class Playlist(val file: PlaylistFile) : Valid
- }
-
- data object Invalid : ExtractedMusic
-}
diff --git a/musikr/src/main/java/org/oxycblt/musikr/pipeline/FlowUtil.kt b/musikr/src/main/java/org/oxycblt/musikr/pipeline/FlowUtil.kt
index 58f2c6eb0..e5cd78a23 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/FlowUtil.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/FlowUtil.kt
@@ -26,46 +26,13 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.withIndex
-internal sealed interface Divert {
- data class Left(val value: L) : Divert
-
- data class Right(val value: R) : Divert
-}
-
-internal class DivertedFlow(
- val manager: Flow,
- val left: Flow,
- val right: Flow
-)
-
-internal inline fun Flow.divert(
- crossinline predicate: (T) -> Divert
-): DivertedFlow {
- val leftChannel = Channel(Channel.UNLIMITED)
- val rightChannel = Channel(Channel.UNLIMITED)
- val managedFlow =
- flow {
- collect {
- when (val result = predicate(it)) {
- is Divert.Left -> leftChannel.send(result.value)
- is Divert.Right -> rightChannel.send(result.value)
- }
- }
- leftChannel.close()
- rightChannel.close()
- }
- return DivertedFlow(managedFlow, leftChannel.receiveAsFlow(), rightChannel.receiveAsFlow())
-}
-
-internal class DistributedFlow(val manager: Flow, val flows: Flow>)
-
/**
* Equally "distributes" the values of some flow across n new flows.
*
* Note that this function requires the "manager" flow to be consumed alongside the split flows in
* order to function. Without this, all of the newly split flows will simply block.
*/
-internal fun Flow.distribute(n: Int): DistributedFlow {
+internal fun Flow.distribute(n: Int): Flow> {
val posChannels = List(n) { Channel(Channel.UNLIMITED) }
val managerFlow =
flow {
@@ -77,6 +44,32 @@ internal fun Flow.distribute(n: Int): DistributedFlow {
channel.close()
}
}
- val hotFlows = posChannels.asFlow().map { it.receiveAsFlow() }
- return DistributedFlow(managerFlow, hotFlows)
+ return (posChannels.map { it.receiveAsFlow() } + managerFlow).asFlow()
}
+
+internal fun Flow>.distributedMap(transform: suspend (T) -> R): Flow> =
+ flow {
+ collect { innerFlow -> emit(innerFlow.tryMap(transform)) }
+ }
+
+internal fun Flow.tryMap(transform: suspend (T) -> R): Flow = flow {
+ collect { value ->
+ try {
+ emit(transform(value))
+ } catch (e: Exception) {
+ throw PipelineException(value, e)
+ }
+ }
+}
+
+internal suspend fun Flow.tryFold(initial: A, operation: suspend (A, T) -> A): A {
+ var accumulator = initial
+ collect { value ->
+ try {
+ accumulator = operation(accumulator, value)
+ } catch (e: Exception) {
+ throw PipelineException(value, e)
+ }
+ }
+ return accumulator
+}
\ No newline at end of file
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 65d7525f5..149e57411 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/PipelineException.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/PipelineException.kt
@@ -18,71 +18,9 @@
package org.oxycblt.musikr.pipeline
-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
-
-class PipelineException(val processing: WhileProcessing, val error: Exception) : Exception() {
+class PipelineException(whileProcessing: Any?, val error: Exception) : Exception() {
override val cause = error
- override val message = "Error while processing ${processing}: ${error.stackTraceToString()}"
+ override val message =
+ "Error while processing ${whileProcessing}: ${error.stackTraceToString()}"
}
-
-sealed interface WhileProcessing {
- class AFile internal constructor(private val file: DeviceFile) : WhileProcessing {
- override fun toString() = "File @ ${file.path}"
- }
-
- class ARawSong internal constructor(private val rawSong: RawSong) : WhileProcessing {
- override fun toString() = "Raw Song @ ${rawSong.file.path}"
- }
-
- class APlaylistFile internal constructor(private val playlist: PlaylistFile) : WhileProcessing {
- override fun toString() = "Playlist File @ ${playlist.name}"
- }
-
- class APreSong internal constructor(private val preSong: PreSong) : WhileProcessing {
- override fun toString() = "Pre Song @ ${preSong.path}"
- }
-
- class APrePlaylist internal constructor(private val prePlaylist: PrePlaylist) :
- WhileProcessing {
- override fun toString() = "Pre Playlist @ ${prePlaylist.name}"
- }
-}
-
-internal suspend fun wrap(file: DeviceFile, block: suspend (DeviceFile) -> R): R =
- try {
- block(file)
- } catch (e: Exception) {
- throw PipelineException(WhileProcessing.AFile(file), e)
- }
-
-internal suspend fun wrap(song: RawSong, block: suspend (RawSong) -> R): R =
- try {
- block(song)
- } catch (e: Exception) {
- throw PipelineException(WhileProcessing.ARawSong(song), e)
- }
-
-internal suspend fun wrap(file: PlaylistFile, block: suspend (PlaylistFile) -> R): R =
- try {
- block(file)
- } catch (e: Exception) {
- throw PipelineException(WhileProcessing.APlaylistFile(file), e)
- }
-
-internal suspend fun wrap(song: PreSong, block: suspend (PreSong) -> R): R =
- try {
- block(song)
- } catch (e: Exception) {
- throw PipelineException(WhileProcessing.APreSong(song), e)
- }
-
-internal suspend fun wrap(playlist: PrePlaylist, block: suspend (PrePlaylist) -> R): R =
- try {
- block(playlist)
- } catch (e: Exception) {
- throw PipelineException(WhileProcessing.APrePlaylist(playlist), e)
- }
diff --git a/musikr/src/main/java/org/oxycblt/musikr/pipeline/PipelineItem.kt b/musikr/src/main/java/org/oxycblt/musikr/pipeline/PipelineItem.kt
new file mode 100644
index 000000000..20cf30221
--- /dev/null
+++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/PipelineItem.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2025 Auxio Project
+ * PipelineItem.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.pipeline
+
+import org.oxycblt.musikr.covers.Cover
+import org.oxycblt.musikr.fs.device.DeviceFile
+import org.oxycblt.musikr.metadata.Properties
+import org.oxycblt.musikr.playlist.PlaylistFile
+import org.oxycblt.musikr.tag.parse.ParsedTags
+
+internal sealed interface PipelineItem
+
+internal sealed interface Incomplete : PipelineItem
+
+internal sealed interface Complete : PipelineItem
+
+internal sealed interface Explored : PipelineItem {
+ sealed interface New : Explored, Incomplete
+
+ sealed interface Known : Explored, Complete
+}
+
+internal data class NewSong(val file: DeviceFile, val addedMs: Long) : Explored.New
+
+internal sealed interface Extracted : PipelineItem {
+ sealed interface Valid : Complete, Extracted
+
+ sealed interface Invalid : Extracted
+}
+
+internal data object InvalidSong : Extracted.Invalid
+
+internal data class RawPlaylist(val file: PlaylistFile) : Explored.Known, Extracted.Valid
+
+internal data class RawSong(
+ val file: DeviceFile,
+ val properties: Properties,
+ val tags: ParsedTags,
+ val cover: Cover?,
+ val addedMs: Long
+) : Explored.Known, Extracted.Valid
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 3c744d51e..62f6f3689 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
@@ -21,7 +21,7 @@ package org.oxycblt.musikr.tag.interpret
import android.net.Uri
import java.util.UUID
import org.oxycblt.musikr.Music
-import org.oxycblt.musikr.cover.Cover
+import org.oxycblt.musikr.covers.Cover
import org.oxycblt.musikr.fs.Format
import org.oxycblt.musikr.fs.Path
import org.oxycblt.musikr.tag.Date
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 0a81ba6f2..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
@@ -68,8 +68,7 @@ private class TagInterpreterImpl(private val interpretation: Interpretation) : T
val songNameOrFileWithoutExt =
song.tags.name ?: requireNotNull(song.file.path.name).split('.').first()
val songNameOrFileWithoutExtCorrect =
- song.tags.name
- ?: requireNotNull(song.file.path.name).split('.').dropLast(1).joinToString(".")
+ 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()
@@ -131,7 +130,7 @@ 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),
+ name = interpretation.naming.name(songNameOrFileWithoutExtCorrect, song.tags.sortName),
rawName = songNameOrFileWithoutExtCorrect,
track = song.tags.track,
disc = song.tags.disc?.let { Disc(it, song.tags.subtitle) },
diff --git a/musikr/src/main/java/org/oxycblt/musikr/tag/parse/ParsedTags.kt b/musikr/src/main/java/org/oxycblt/musikr/tag/parse/ParsedTags.kt
index 1d7198a56..70ec61959 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/tag/parse/ParsedTags.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/tag/parse/ParsedTags.kt
@@ -20,7 +20,7 @@ package org.oxycblt.musikr.tag.parse
import org.oxycblt.musikr.tag.Date
-internal data class ParsedTags(
+data class ParsedTags(
val durationMs: Long,
val replayGainTrackAdjustment: Float? = null,
val replayGainAlbumAdjustment: Float? = null,