music: implement revisioned covers
This commit is contained in:
parent
8b69042288
commit
1843986f75
11 changed files with 142 additions and 97 deletions
|
@ -47,7 +47,7 @@ import org.oxycblt.musikr.cover.Cover
|
||||||
import org.oxycblt.musikr.cover.StoredCovers
|
import org.oxycblt.musikr.cover.StoredCovers
|
||||||
|
|
||||||
class CoverKeyer @Inject constructor() : Keyer<Cover> {
|
class CoverKeyer @Inject constructor() : Keyer<Cover> {
|
||||||
override fun key(data: Cover, options: Options) = "${data.key}&${options.size}"
|
override fun key(data: Cover, options: Options) = "${data.id}&${options.size}"
|
||||||
}
|
}
|
||||||
|
|
||||||
class CoverFetcher
|
class CoverFetcher
|
||||||
|
@ -56,16 +56,14 @@ private constructor(
|
||||||
private val cover: Cover,
|
private val cover: Cover,
|
||||||
private val size: Size,
|
private val size: Size,
|
||||||
) : Fetcher {
|
) : Fetcher {
|
||||||
private val storedCovers = StoredCovers.from(context, "covers")
|
|
||||||
|
|
||||||
override suspend fun fetch(): FetchResult? {
|
override suspend fun fetch(): FetchResult? {
|
||||||
val streams =
|
val streams =
|
||||||
when (val cover = cover) {
|
when (val cover = cover) {
|
||||||
is Cover.Single -> listOfNotNull(storedCovers.read(cover))
|
is Cover.Single -> listOfNotNull(cover.open())
|
||||||
is Cover.Multi ->
|
is Cover.Multi ->
|
||||||
buildList {
|
buildList {
|
||||||
for (single in cover.all) {
|
for (single in cover.all) {
|
||||||
storedCovers.read(single)?.let { add(it) }
|
single.open()?.let { add(it) }
|
||||||
if (size == 4) {
|
if (size == 4) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,8 +30,10 @@ import kotlinx.coroutines.yield
|
||||||
import org.oxycblt.auxio.music.MusicRepository.IndexingWorker
|
import org.oxycblt.auxio.music.MusicRepository.IndexingWorker
|
||||||
import org.oxycblt.musikr.IndexingProgress
|
import org.oxycblt.musikr.IndexingProgress
|
||||||
import org.oxycblt.musikr.Interpretation
|
import org.oxycblt.musikr.Interpretation
|
||||||
|
import org.oxycblt.musikr.Library
|
||||||
import org.oxycblt.musikr.Music
|
import org.oxycblt.musikr.Music
|
||||||
import org.oxycblt.musikr.Musikr
|
import org.oxycblt.musikr.Musikr
|
||||||
|
import org.oxycblt.musikr.MutableLibrary
|
||||||
import org.oxycblt.musikr.Playlist
|
import org.oxycblt.musikr.Playlist
|
||||||
import org.oxycblt.musikr.Song
|
import org.oxycblt.musikr.Song
|
||||||
import org.oxycblt.musikr.Storage
|
import org.oxycblt.musikr.Storage
|
||||||
|
@ -57,7 +59,7 @@ import timber.log.Timber as L
|
||||||
*/
|
*/
|
||||||
interface MusicRepository {
|
interface MusicRepository {
|
||||||
/** The current library */
|
/** The current library */
|
||||||
val library: RevisionedLibrary?
|
val library: Library?
|
||||||
|
|
||||||
/** The current state of music loading. Null if no load has occurred yet. */
|
/** The current state of music loading. Null if no load has occurred yet. */
|
||||||
val indexingState: IndexingState?
|
val indexingState: IndexingState?
|
||||||
|
@ -222,7 +224,7 @@ constructor(
|
||||||
private val indexingListeners = mutableListOf<MusicRepository.IndexingListener>()
|
private val indexingListeners = mutableListOf<MusicRepository.IndexingListener>()
|
||||||
@Volatile private var indexingWorker: MusicRepository.IndexingWorker? = null
|
@Volatile private var indexingWorker: MusicRepository.IndexingWorker? = null
|
||||||
|
|
||||||
@Volatile override var library: MutableRevisionedLibrary? = null
|
@Volatile override var library: MutableLibrary? = null
|
||||||
@Volatile private var previousCompletedState: IndexingState.Completed? = null
|
@Volatile private var previousCompletedState: IndexingState.Completed? = null
|
||||||
@Volatile private var currentIndexingState: IndexingState? = null
|
@Volatile private var currentIndexingState: IndexingState? = null
|
||||||
override val indexingState: IndexingState?
|
override val indexingState: IndexingState?
|
||||||
|
@ -365,18 +367,18 @@ constructor(
|
||||||
val revision: UUID
|
val revision: UUID
|
||||||
val storage: Storage
|
val storage: Storage
|
||||||
if (withCache) {
|
if (withCache) {
|
||||||
revision = this.library?.revision ?: musicSettings.revision
|
revision = musicSettings.revision
|
||||||
storage =
|
storage =
|
||||||
Storage(
|
Storage(
|
||||||
Cache.full(cacheDatabase),
|
Cache.full(cacheDatabase),
|
||||||
StoredCovers.from(context, "covers_$revision"),
|
MutableRevisionedStoredCovers(context, revision),
|
||||||
StoredPlaylists.from(playlistDatabase))
|
StoredPlaylists.from(playlistDatabase))
|
||||||
} else {
|
} else {
|
||||||
revision = UUID.randomUUID()
|
revision = UUID.randomUUID()
|
||||||
storage =
|
storage =
|
||||||
Storage(
|
Storage(
|
||||||
Cache.writeOnly(cacheDatabase),
|
Cache.writeOnly(cacheDatabase),
|
||||||
StoredCovers.from(context, "covers_$revision"),
|
MutableRevisionedStoredCovers(context, revision),
|
||||||
StoredPlaylists.from(playlistDatabase))
|
StoredPlaylists.from(playlistDatabase))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -385,8 +387,6 @@ constructor(
|
||||||
val newLibrary =
|
val newLibrary =
|
||||||
Musikr.new(context, storage, interpretation).run(locations, ::emitIndexingProgress)
|
Musikr.new(context, storage, interpretation).run(locations, ::emitIndexingProgress)
|
||||||
|
|
||||||
val revisionedLibrary = MutableRevisionedLibrary(revision, newLibrary)
|
|
||||||
|
|
||||||
emitIndexingCompletion(null)
|
emitIndexingCompletion(null)
|
||||||
|
|
||||||
// We want to make sure that all reads and writes are synchronized due to the sheer
|
// We want to make sure that all reads and writes are synchronized due to the sheer
|
||||||
|
@ -408,7 +408,7 @@ constructor(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.library = revisionedLibrary
|
this.library = newLibrary
|
||||||
}
|
}
|
||||||
|
|
||||||
// Consumers expect their updates to be on the main thread (notably PlaybackService),
|
// Consumers expect their updates to be on the main thread (notably PlaybackService),
|
||||||
|
@ -418,9 +418,7 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Quietly update the revision if needed (this way we don't disrupt any new loads)
|
// Quietly update the revision if needed (this way we don't disrupt any new loads)
|
||||||
if (!withCache) {
|
musicSettings.revision = revision
|
||||||
musicSettings.revision = revisionedLibrary.revision
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun emitIndexingProgress(progress: IndexingProgress) {
|
private suspend fun emitIndexingProgress(progress: IndexingProgress) {
|
||||||
|
|
|
@ -15,33 +15,59 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.music
|
package org.oxycblt.auxio.music
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.media3.common.util.Log
|
||||||
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import org.oxycblt.musikr.Library
|
import org.oxycblt.musikr.cover.Cover
|
||||||
import org.oxycblt.musikr.MutableLibrary
|
import org.oxycblt.musikr.cover.MutableStoredCovers
|
||||||
import org.oxycblt.musikr.Playlist
|
import org.oxycblt.musikr.cover.StoredCovers
|
||||||
import org.oxycblt.musikr.Song
|
|
||||||
|
|
||||||
interface RevisionedLibrary : Library {
|
open class RevisionedStoredCovers(
|
||||||
val revision: UUID
|
private val context: Context,
|
||||||
|
private val revision: UUID?
|
||||||
|
) : StoredCovers {
|
||||||
|
protected val inner = revision?.let { StoredCovers.at(context, "covers_$it") }
|
||||||
|
|
||||||
|
override suspend fun obtain(id: String): RevisionedCover? {
|
||||||
|
val split = id.split('@', limit = 2)
|
||||||
|
if (split.size != 2) return null
|
||||||
|
val (coverId, coverRevisionStr) = split
|
||||||
|
val coverRevision = coverRevisionStr.toUuidOrNull() ?: return null
|
||||||
|
Log.d("RevisionedStoredCovers", "$coverId $coverRevision $revision")
|
||||||
|
if (revision != null) {
|
||||||
|
if (coverRevision != revision) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val storedCovers = unlikelyToBeNull(inner)
|
||||||
|
return storedCovers.obtain(coverId)?.let { RevisionedCover(revision, it) }
|
||||||
|
} else {
|
||||||
|
val storedCovers = StoredCovers.at(context, "covers_$coverRevision")
|
||||||
|
return storedCovers.obtain(coverId)?.let { RevisionedCover(coverRevision, it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MutableRevisionedLibrary(override val revision: UUID, private val inner: MutableLibrary) :
|
class MutableRevisionedStoredCovers(
|
||||||
RevisionedLibrary, Library by inner, MutableLibrary {
|
context: Context,
|
||||||
override suspend fun createPlaylist(name: String, songs: List<Song>) =
|
private val revision: UUID
|
||||||
MutableRevisionedLibrary(revision, inner.createPlaylist(name, songs))
|
) : RevisionedStoredCovers(context, revision), MutableStoredCovers {
|
||||||
|
override suspend fun write(data: ByteArray): RevisionedCover? {
|
||||||
override suspend fun renamePlaylist(playlist: Playlist, name: String) =
|
return unlikelyToBeNull(inner).write(data)?.let { RevisionedCover(revision, it) }
|
||||||
MutableRevisionedLibrary(revision, inner.renamePlaylist(playlist, name))
|
}
|
||||||
|
|
||||||
override suspend fun addToPlaylist(playlist: Playlist, songs: List<Song>) =
|
|
||||||
MutableRevisionedLibrary(revision, inner.addToPlaylist(playlist, songs))
|
|
||||||
|
|
||||||
override suspend fun rewritePlaylist(playlist: Playlist, songs: List<Song>) =
|
|
||||||
MutableRevisionedLibrary(revision, inner.rewritePlaylist(playlist, songs))
|
|
||||||
|
|
||||||
override suspend fun deletePlaylist(playlist: Playlist) =
|
|
||||||
MutableRevisionedLibrary(revision, inner.deletePlaylist(playlist))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class RevisionedCover(private val revision: UUID, val inner: Cover.Single) : Cover.Single by inner {
|
||||||
|
override val id: String
|
||||||
|
get() = "${inner.id}@${revision}"
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun String.toUuidOrNull(): UUID? =
|
||||||
|
try {
|
||||||
|
UUID.fromString(this)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
package org.oxycblt.musikr
|
package org.oxycblt.musikr
|
||||||
|
|
||||||
import org.oxycblt.musikr.cache.Cache
|
import org.oxycblt.musikr.cache.Cache
|
||||||
|
import org.oxycblt.musikr.cover.MutableStoredCovers
|
||||||
import org.oxycblt.musikr.cover.StoredCovers
|
import org.oxycblt.musikr.cover.StoredCovers
|
||||||
import org.oxycblt.musikr.playlist.db.StoredPlaylists
|
import org.oxycblt.musikr.playlist.db.StoredPlaylists
|
||||||
import org.oxycblt.musikr.tag.interpret.Naming
|
import org.oxycblt.musikr.tag.interpret.Naming
|
||||||
|
@ -26,7 +27,7 @@ import org.oxycblt.musikr.tag.interpret.Separators
|
||||||
|
|
||||||
data class Storage(
|
data class Storage(
|
||||||
val cache: Cache,
|
val cache: Cache,
|
||||||
val storedCovers: StoredCovers,
|
val storedCovers: MutableStoredCovers,
|
||||||
val storedPlaylists: StoredPlaylists
|
val storedPlaylists: StoredPlaylists
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -18,11 +18,12 @@
|
||||||
|
|
||||||
package org.oxycblt.musikr.cache
|
package org.oxycblt.musikr.cache
|
||||||
|
|
||||||
|
import org.oxycblt.musikr.cover.StoredCovers
|
||||||
import org.oxycblt.musikr.fs.DeviceFile
|
import org.oxycblt.musikr.fs.DeviceFile
|
||||||
import org.oxycblt.musikr.pipeline.RawSong
|
import org.oxycblt.musikr.pipeline.RawSong
|
||||||
|
|
||||||
interface Cache {
|
interface Cache {
|
||||||
suspend fun read(file: DeviceFile): CacheResult
|
suspend fun read(file: DeviceFile, storedCovers: StoredCovers): CacheResult
|
||||||
|
|
||||||
suspend fun write(song: RawSong)
|
suspend fun write(song: RawSong)
|
||||||
|
|
||||||
|
@ -40,9 +41,9 @@ sealed interface CacheResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
private class FullCache(private val cacheInfoDao: CacheInfoDao) : Cache {
|
private class FullCache(private val cacheInfoDao: CacheInfoDao) : Cache {
|
||||||
override suspend fun read(file: DeviceFile) =
|
override suspend fun read(file: DeviceFile, storedCovers: StoredCovers) =
|
||||||
cacheInfoDao.selectSong(file.uri.toString(), file.lastModified)?.let {
|
cacheInfoDao.selectSong(file.uri.toString(), file.lastModified)?.let {
|
||||||
CacheResult.Hit(it.intoRawSong(file))
|
CacheResult.Hit(it.intoRawSong(file, storedCovers))
|
||||||
} ?: CacheResult.Miss(file)
|
} ?: CacheResult.Miss(file)
|
||||||
|
|
||||||
override suspend fun write(song: RawSong) =
|
override suspend fun write(song: RawSong) =
|
||||||
|
@ -50,7 +51,7 @@ private class FullCache(private val cacheInfoDao: CacheInfoDao) : Cache {
|
||||||
}
|
}
|
||||||
|
|
||||||
private class WriteOnlyCache(private val cacheInfoDao: CacheInfoDao) : Cache {
|
private class WriteOnlyCache(private val cacheInfoDao: CacheInfoDao) : Cache {
|
||||||
override suspend fun read(file: DeviceFile) = CacheResult.Miss(file)
|
override suspend fun read(file: DeviceFile, storedCovers: StoredCovers) = CacheResult.Miss(file)
|
||||||
|
|
||||||
override suspend fun write(song: RawSong) =
|
override suspend fun write(song: RawSong) =
|
||||||
cacheInfoDao.updateSong(CachedSong.fromRawSong(song))
|
cacheInfoDao.updateSong(CachedSong.fromRawSong(song))
|
||||||
|
|
|
@ -31,6 +31,7 @@ import androidx.room.RoomDatabase
|
||||||
import androidx.room.TypeConverter
|
import androidx.room.TypeConverter
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
import org.oxycblt.musikr.cover.Cover
|
import org.oxycblt.musikr.cover.Cover
|
||||||
|
import org.oxycblt.musikr.cover.StoredCovers
|
||||||
import org.oxycblt.musikr.fs.DeviceFile
|
import org.oxycblt.musikr.fs.DeviceFile
|
||||||
import org.oxycblt.musikr.metadata.Properties
|
import org.oxycblt.musikr.metadata.Properties
|
||||||
import org.oxycblt.musikr.pipeline.RawSong
|
import org.oxycblt.musikr.pipeline.RawSong
|
||||||
|
@ -89,9 +90,9 @@ internal data class CachedSong(
|
||||||
val genreNames: List<String>,
|
val genreNames: List<String>,
|
||||||
val replayGainTrackAdjustment: Float?,
|
val replayGainTrackAdjustment: Float?,
|
||||||
val replayGainAlbumAdjustment: Float?,
|
val replayGainAlbumAdjustment: Float?,
|
||||||
val cover: Cover.Single?,
|
val coverId: String?,
|
||||||
) {
|
) {
|
||||||
fun intoRawSong(file: DeviceFile) =
|
suspend fun intoRawSong(file: DeviceFile, storedCovers: StoredCovers) =
|
||||||
RawSong(
|
RawSong(
|
||||||
file,
|
file,
|
||||||
Properties(mimeType, durationMs, bitrateHz, sampleRateHz),
|
Properties(mimeType, durationMs, bitrateHz, sampleRateHz),
|
||||||
|
@ -117,7 +118,7 @@ internal data class CachedSong(
|
||||||
genreNames = genreNames,
|
genreNames = genreNames,
|
||||||
replayGainTrackAdjustment = replayGainTrackAdjustment,
|
replayGainTrackAdjustment = replayGainTrackAdjustment,
|
||||||
replayGainAlbumAdjustment = replayGainAlbumAdjustment),
|
replayGainAlbumAdjustment = replayGainAlbumAdjustment),
|
||||||
cover)
|
coverId?.let { storedCovers.obtain(it) })
|
||||||
|
|
||||||
object Converters {
|
object Converters {
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
|
@ -130,10 +131,6 @@ internal data class CachedSong(
|
||||||
@TypeConverter fun fromDate(date: Date?) = date?.toString()
|
@TypeConverter fun fromDate(date: Date?) = date?.toString()
|
||||||
|
|
||||||
@TypeConverter fun toDate(string: String?) = string?.let(Date::from)
|
@TypeConverter fun toDate(string: String?) = string?.let(Date::from)
|
||||||
|
|
||||||
@TypeConverter fun fromCover(cover: Cover.Single?) = cover?.key
|
|
||||||
|
|
||||||
@TypeConverter fun toCover(key: String?) = key?.let { Cover.Single(it) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -162,7 +159,7 @@ internal data class CachedSong(
|
||||||
genreNames = rawSong.tags.genreNames,
|
genreNames = rawSong.tags.genreNames,
|
||||||
replayGainTrackAdjustment = rawSong.tags.replayGainTrackAdjustment,
|
replayGainTrackAdjustment = rawSong.tags.replayGainTrackAdjustment,
|
||||||
replayGainAlbumAdjustment = rawSong.tags.replayGainAlbumAdjustment,
|
replayGainAlbumAdjustment = rawSong.tags.replayGainAlbumAdjustment,
|
||||||
cover = rawSong.cover,
|
coverId = rawSong.cover?.id,
|
||||||
mimeType = rawSong.properties.mimeType,
|
mimeType = rawSong.properties.mimeType,
|
||||||
bitrateHz = rawSong.properties.bitrateKbps,
|
bitrateHz = rawSong.properties.bitrateKbps,
|
||||||
sampleRateHz = rawSong.properties.sampleRateHz)
|
sampleRateHz = rawSong.properties.sampleRateHz)
|
||||||
|
|
|
@ -19,27 +19,26 @@
|
||||||
package org.oxycblt.musikr.cover
|
package org.oxycblt.musikr.cover
|
||||||
|
|
||||||
import org.oxycblt.musikr.Song
|
import org.oxycblt.musikr.Song
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
sealed interface Cover {
|
sealed interface Cover {
|
||||||
val key: String
|
val id: String
|
||||||
|
|
||||||
data class Single(override val key: String) : Cover
|
interface Single : Cover {
|
||||||
|
suspend fun open(): InputStream?
|
||||||
|
}
|
||||||
|
|
||||||
class Multi(val all: List<Single>) : Cover {
|
class Multi(val all: List<Single>) : Cover {
|
||||||
override val key = "multi@${all.hashCode()}"
|
override val id = "multi@${all.hashCode()}"
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun nil() = Multi(listOf())
|
|
||||||
|
|
||||||
fun single(key: String) = Single(key)
|
|
||||||
|
|
||||||
fun multi(songs: Collection<Song>) = order(songs).run { Multi(this) }
|
fun multi(songs: Collection<Song>) = order(songs).run { Multi(this) }
|
||||||
|
|
||||||
private fun order(songs: Collection<Song>) =
|
private fun order(songs: Collection<Song>) =
|
||||||
songs
|
songs
|
||||||
.mapNotNull { it.cover }
|
.mapNotNull { it.cover }
|
||||||
.groupBy { it.key }
|
.groupBy { it.id }
|
||||||
.entries
|
.entries
|
||||||
.sortedByDescending { it.key }
|
.sortedByDescending { it.key }
|
||||||
.sortedByDescending { it.value.size }
|
.sortedByDescending { it.value.size }
|
||||||
|
|
|
@ -15,29 +15,34 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.musikr.cover
|
package org.oxycblt.musikr.cover
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
|
||||||
internal interface CoverFiles {
|
interface CoverFiles {
|
||||||
suspend fun read(id: String): InputStream?
|
suspend fun find(id: String): CoverFile?
|
||||||
|
suspend fun write(id: String, data: ByteArray): CoverFile?
|
||||||
suspend fun write(id: String, data: ByteArray)
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun from(context: Context, path: String, format: CoverFormat): CoverFiles =
|
fun at(context: Context, path: String): CoverFiles =
|
||||||
CoverFilesImpl(File(context.filesDir, path).also { it.mkdirs() }, format)
|
CoverFilesImpl(File(context.filesDir, path).also { it.mkdirs() }, CoverFormat.webp())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CoverFile {
|
||||||
|
suspend fun open(): InputStream?
|
||||||
|
}
|
||||||
|
|
||||||
private class CoverFilesImpl(private val dir: File, private val coverFormat: CoverFormat) :
|
private class CoverFilesImpl(private val dir: File, private val coverFormat: CoverFormat) :
|
||||||
CoverFiles {
|
CoverFiles {
|
||||||
private val fileMutexes = mutableMapOf<String, Mutex>()
|
private val fileMutexes = mutableMapOf<String, Mutex>()
|
||||||
|
@ -47,32 +52,34 @@ private class CoverFilesImpl(private val dir: File, private val coverFormat: Cov
|
||||||
return mapMutex.withLock { fileMutexes.getOrPut(file) { Mutex() } }
|
return mapMutex.withLock { fileMutexes.getOrPut(file) { Mutex() } }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun read(id: String): InputStream? =
|
override suspend fun find(id: String): CoverFile? =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
File(dir, getTargetFilePath(id)).inputStream()
|
File(dir, getTargetFilePath(id)).takeIf { it.exists() }?.let { CoverFileImpl(it) }
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun write(id: String, data: ByteArray) {
|
override suspend fun write(id: String, data: ByteArray): CoverFile? {
|
||||||
val fileMutex = getMutexForFile(id)
|
val fileMutex = getMutexForFile(id)
|
||||||
|
return fileMutex.withLock {
|
||||||
fileMutex.withLock {
|
|
||||||
val targetFile = File(dir, getTargetFilePath(id))
|
val targetFile = File(dir, getTargetFilePath(id))
|
||||||
if (targetFile.exists()) {
|
if (!targetFile.exists()) {
|
||||||
return
|
withContext(Dispatchers.IO) {
|
||||||
}
|
val tempFile = File(dir, getTempFilePath(id))
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
val tempFile = File(dir, getTempFilePath(id))
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
tempFile.outputStream().use { coverFormat.transcodeInto(data, it) }
|
tempFile.outputStream().use { coverFormat.transcodeInto(data, it) }
|
||||||
tempFile.renameTo(targetFile)
|
tempFile.renameTo(targetFile)
|
||||||
} catch (e: IOException) {
|
CoverFileImpl(targetFile)
|
||||||
tempFile.delete()
|
} catch (e: IOException) {
|
||||||
|
tempFile.delete()
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
CoverFileImpl(targetFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -81,3 +88,7 @@ private class CoverFilesImpl(private val dir: File, private val coverFormat: Cov
|
||||||
|
|
||||||
private fun getTempFilePath(name: String) = "${getTargetFilePath(name)}.tmp"
|
private fun getTempFilePath(name: String) = "${getTargetFilePath(name)}.tmp"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class CoverFileImpl(private val file: File) : CoverFile {
|
||||||
|
override suspend fun open() = withContext(Dispatchers.IO) { file.inputStream() }
|
||||||
|
}
|
|
@ -15,33 +15,42 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.musikr.cover
|
package org.oxycblt.musikr.cover
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
interface StoredCovers {
|
interface StoredCovers {
|
||||||
suspend fun read(cover: Cover.Single): InputStream?
|
suspend fun obtain(id: String): Cover.Single?
|
||||||
|
|
||||||
suspend fun write(data: ByteArray): Cover.Single?
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun from(context: Context, path: String): StoredCovers =
|
fun at(context: Context, path: String): MutableStoredCovers =
|
||||||
FileStoredCovers(
|
FileStoredCovers(
|
||||||
CoverIdentifier.md5(), CoverFiles.from(context, path, CoverFormat.webp()))
|
CoverIdentifier.md5(), CoverFiles.at(context, path)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MutableStoredCovers : StoredCovers {
|
||||||
|
suspend fun write(data: ByteArray): Cover.Single?
|
||||||
|
}
|
||||||
|
|
||||||
private class FileStoredCovers(
|
private class FileStoredCovers(
|
||||||
private val coverIdentifier: CoverIdentifier,
|
private val coverIdentifier: CoverIdentifier,
|
||||||
private val coverFiles: CoverFiles
|
private val coverFiles: CoverFiles
|
||||||
) : StoredCovers {
|
) : StoredCovers, MutableStoredCovers {
|
||||||
override suspend fun read(cover: Cover.Single) = coverFiles.read(cover.key)
|
override suspend fun obtain(id: String) = coverFiles.find(id)?.let { FileCover(id, it) }
|
||||||
|
|
||||||
override suspend fun write(data: ByteArray) =
|
override suspend fun write(data: ByteArray) =
|
||||||
coverIdentifier.identify(data).let { key ->
|
coverIdentifier.identify(data).let { id ->
|
||||||
coverFiles.write(key, data)
|
coverFiles.write(id, data)?.let { FileCover(id, it) }
|
||||||
Cover.Single(key)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class FileCover(
|
||||||
|
override val id: String,
|
||||||
|
private val coverFile: CoverFile
|
||||||
|
) : Cover.Single {
|
||||||
|
override suspend fun open() = coverFile.open()
|
||||||
|
}
|
|
@ -18,6 +18,7 @@
|
||||||
|
|
||||||
package org.oxycblt.musikr.pipeline
|
package org.oxycblt.musikr.pipeline
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
@ -78,7 +79,10 @@ private class EvaluateStepImpl(
|
||||||
val graphBuild =
|
val graphBuild =
|
||||||
merge(
|
merge(
|
||||||
filterFlow.manager,
|
filterFlow.manager,
|
||||||
preSongs.onEach { wrap(it, graphBuilder::add) },
|
preSongs.onEach {
|
||||||
|
Log.d("EvaluateStep", it.toString())
|
||||||
|
wrap(it, graphBuilder::add)
|
||||||
|
},
|
||||||
prePlaylists.onEach { wrap(it, graphBuilder::add) })
|
prePlaylists.onEach { wrap(it, graphBuilder::add) })
|
||||||
graphBuild.collect()
|
graphBuild.collect()
|
||||||
val graph = graphBuilder.build()
|
val graph = graphBuilder.build()
|
||||||
|
|
|
@ -32,6 +32,7 @@ import org.oxycblt.musikr.Storage
|
||||||
import org.oxycblt.musikr.cache.Cache
|
import org.oxycblt.musikr.cache.Cache
|
||||||
import org.oxycblt.musikr.cache.CacheResult
|
import org.oxycblt.musikr.cache.CacheResult
|
||||||
import org.oxycblt.musikr.cover.Cover
|
import org.oxycblt.musikr.cover.Cover
|
||||||
|
import org.oxycblt.musikr.cover.MutableStoredCovers
|
||||||
import org.oxycblt.musikr.cover.StoredCovers
|
import org.oxycblt.musikr.cover.StoredCovers
|
||||||
import org.oxycblt.musikr.fs.DeviceFile
|
import org.oxycblt.musikr.fs.DeviceFile
|
||||||
import org.oxycblt.musikr.metadata.MetadataExtractor
|
import org.oxycblt.musikr.metadata.MetadataExtractor
|
||||||
|
@ -59,7 +60,7 @@ private class ExtractStepImpl(
|
||||||
private val metadataExtractor: MetadataExtractor,
|
private val metadataExtractor: MetadataExtractor,
|
||||||
private val tagParser: TagParser,
|
private val tagParser: TagParser,
|
||||||
private val cache: Cache,
|
private val cache: Cache,
|
||||||
private val storedCovers: StoredCovers
|
private val storedCovers: MutableStoredCovers
|
||||||
) : ExtractStep {
|
) : ExtractStep {
|
||||||
override fun extract(nodes: Flow<ExploreNode>): Flow<ExtractedMusic> {
|
override fun extract(nodes: Flow<ExploreNode>): Flow<ExtractedMusic> {
|
||||||
val filterFlow =
|
val filterFlow =
|
||||||
|
@ -74,7 +75,7 @@ private class ExtractStepImpl(
|
||||||
|
|
||||||
val cacheResults =
|
val cacheResults =
|
||||||
audioNodes
|
audioNodes
|
||||||
.map { wrap(it, cache::read) }
|
.map { wrap(it) { file -> cache.read(file, storedCovers)} }
|
||||||
.flowOn(Dispatchers.IO)
|
.flowOn(Dispatchers.IO)
|
||||||
.buffer(Channel.UNLIMITED)
|
.buffer(Channel.UNLIMITED)
|
||||||
val cacheFlow =
|
val cacheFlow =
|
||||||
|
|
Loading…
Reference in a new issue