musikr: add date added support
Through a new `Tracker` interface. Tracker is kind of a generic name. It's set up in the case that I have to wind up associating more post-extraction metadata with songs.
This commit is contained in:
parent
c42ac644eb
commit
ca6388b28d
12 changed files with 158 additions and 29 deletions
|
@ -28,6 +28,7 @@ import dagger.hilt.components.SingletonComponent
|
|||
import javax.inject.Singleton
|
||||
import org.oxycblt.musikr.cache.Cache
|
||||
import org.oxycblt.musikr.playlist.db.StoredPlaylists
|
||||
import org.oxycblt.musikr.track.Tracker
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
|
@ -42,6 +43,8 @@ interface MusicModule {
|
|||
class MusikrShimModule {
|
||||
@Singleton @Provides fun cache(@ApplicationContext context: Context) = Cache.from(context)
|
||||
|
||||
@Singleton @Provides fun tracker(@ApplicationContext context: Context) = Tracker.from(context)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun storedPlaylists(@ApplicationContext context: Context) = StoredPlaylists.from(context)
|
||||
|
|
|
@ -42,6 +42,7 @@ import org.oxycblt.musikr.cache.WriteOnlyCache
|
|||
import org.oxycblt.musikr.playlist.db.StoredPlaylists
|
||||
import org.oxycblt.musikr.tag.interpret.Naming
|
||||
import org.oxycblt.musikr.tag.interpret.Separators
|
||||
import org.oxycblt.musikr.track.Tracker
|
||||
import timber.log.Timber as L
|
||||
|
||||
/**
|
||||
|
@ -215,6 +216,7 @@ class MusicRepositoryImpl
|
|||
constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val cache: Cache,
|
||||
private val tracker: Tracker,
|
||||
private val storedPlaylists: StoredPlaylists,
|
||||
private val musicSettings: MusicSettings
|
||||
) : MusicRepository {
|
||||
|
@ -365,7 +367,7 @@ constructor(
|
|||
val newRevision = currentRevision?.takeIf { withCache } ?: UUID.randomUUID()
|
||||
val cache = if (withCache) cache else WriteOnlyCache(cache)
|
||||
val covers = MutableRevisionedStoredCovers(context, newRevision)
|
||||
val storage = Storage(cache, covers, storedPlaylists)
|
||||
val storage = Storage(cache, tracker, covers, storedPlaylists)
|
||||
val interpretation = Interpretation(nameFactory, separators)
|
||||
|
||||
val newLibrary =
|
||||
|
|
|
@ -23,9 +23,11 @@ import org.oxycblt.musikr.cover.MutableStoredCovers
|
|||
import org.oxycblt.musikr.playlist.db.StoredPlaylists
|
||||
import org.oxycblt.musikr.tag.interpret.Naming
|
||||
import org.oxycblt.musikr.tag.interpret.Separators
|
||||
import org.oxycblt.musikr.track.Tracker
|
||||
|
||||
data class Storage(
|
||||
val cache: Cache,
|
||||
val tracker: Tracker,
|
||||
val storedCovers: MutableStoredCovers,
|
||||
val storedPlaylists: StoredPlaylists
|
||||
)
|
||||
|
|
|
@ -24,7 +24,7 @@ import org.oxycblt.musikr.playlist.interpret.PrePlaylist
|
|||
import org.oxycblt.musikr.tag.interpret.PreAlbum
|
||||
import org.oxycblt.musikr.tag.interpret.PreArtist
|
||||
import org.oxycblt.musikr.tag.interpret.PreGenre
|
||||
import org.oxycblt.musikr.tag.interpret.PreSong
|
||||
import org.oxycblt.musikr.track.TrackedSong
|
||||
import org.oxycblt.musikr.util.unlikelyToBeNull
|
||||
|
||||
internal data class MusicGraph(
|
||||
|
@ -35,7 +35,7 @@ internal data class MusicGraph(
|
|||
val playlistVertex: Set<PlaylistVertex>
|
||||
) {
|
||||
interface Builder {
|
||||
fun add(preSong: PreSong)
|
||||
fun add(trackedSong: TrackedSong)
|
||||
|
||||
fun add(prePlaylist: PrePlaylist)
|
||||
|
||||
|
@ -54,7 +54,8 @@ private class MusicGraphBuilderImpl : MusicGraph.Builder {
|
|||
private val genreVertices = mutableMapOf<PreGenre, GenreVertex>()
|
||||
private val playlistVertices = mutableSetOf<PlaylistVertex>()
|
||||
|
||||
override fun add(preSong: PreSong) {
|
||||
override fun add(trackedSong: TrackedSong) {
|
||||
val preSong = trackedSong.preSong
|
||||
val uid = preSong.uid
|
||||
if (songVertices.containsKey(uid)) {
|
||||
return
|
||||
|
@ -88,7 +89,7 @@ private class MusicGraphBuilderImpl : MusicGraph.Builder {
|
|||
|
||||
val songVertex =
|
||||
SongVertex(
|
||||
preSong,
|
||||
trackedSong,
|
||||
albumVertex,
|
||||
songArtistVertices.toMutableList(),
|
||||
songGenreVertices.toMutableList())
|
||||
|
@ -311,7 +312,7 @@ private class MusicGraphBuilderImpl : MusicGraph.Builder {
|
|||
}
|
||||
|
||||
internal class SongVertex(
|
||||
val preSong: PreSong,
|
||||
val trackedSong: TrackedSong,
|
||||
var albumVertex: AlbumVertex,
|
||||
var artistVertices: MutableList<ArtistVertex>,
|
||||
var genreVertices: MutableList<GenreVertex>
|
||||
|
|
|
@ -75,7 +75,7 @@ private class LibraryFactoryImpl() : LibraryFactory {
|
|||
}
|
||||
|
||||
private class SongVertexCore(private val vertex: SongVertex) : SongCore {
|
||||
override val preSong = vertex.preSong
|
||||
override val trackedSong = vertex.trackedSong
|
||||
|
||||
override fun resolveAlbum() = vertex.albumVertex.tag as Album
|
||||
|
||||
|
|
|
@ -22,10 +22,10 @@ import org.oxycblt.musikr.Album
|
|||
import org.oxycblt.musikr.Artist
|
||||
import org.oxycblt.musikr.Genre
|
||||
import org.oxycblt.musikr.Song
|
||||
import org.oxycblt.musikr.tag.interpret.PreSong
|
||||
import org.oxycblt.musikr.track.TrackedSong
|
||||
|
||||
internal interface SongCore {
|
||||
val preSong: PreSong
|
||||
val trackedSong: TrackedSong
|
||||
|
||||
fun resolveAlbum(): Album
|
||||
|
||||
|
@ -40,7 +40,7 @@ internal interface SongCore {
|
|||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
internal class SongImpl(private val handle: SongCore) : Song {
|
||||
private val preSong = handle.preSong
|
||||
private val preSong = handle.trackedSong.preSong
|
||||
|
||||
override val uid = preSong.uid
|
||||
override val name = preSong.name
|
||||
|
@ -56,7 +56,7 @@ internal class SongImpl(private val handle: SongCore) : Song {
|
|||
override val sampleRateHz = preSong.sampleRateHz
|
||||
override val replayGainAdjustment = preSong.replayGainAdjustment
|
||||
override val lastModified = preSong.lastModified
|
||||
override val dateAdded = preSong.dateAdded
|
||||
override val dateAdded = handle.trackedSong.dateAdded
|
||||
override val cover = preSong.cover
|
||||
override val album: Album
|
||||
get() = handle.resolveAlbum()
|
||||
|
|
|
@ -19,10 +19,12 @@
|
|||
package org.oxycblt.musikr.pipeline
|
||||
|
||||
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.collect
|
||||
import kotlinx.coroutines.flow.flattenMerge
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.merge
|
||||
|
@ -35,6 +37,7 @@ import org.oxycblt.musikr.model.LibraryFactory
|
|||
import org.oxycblt.musikr.playlist.db.StoredPlaylists
|
||||
import org.oxycblt.musikr.playlist.interpret.PlaylistInterpreter
|
||||
import org.oxycblt.musikr.tag.interpret.TagInterpreter
|
||||
import org.oxycblt.musikr.track.Tracker
|
||||
|
||||
internal interface EvaluateStep {
|
||||
suspend fun evaluate(extractedMusic: Flow<ExtractedMusic>): MutableLibrary
|
||||
|
@ -43,14 +46,17 @@ internal interface EvaluateStep {
|
|||
fun new(storage: Storage, interpretation: Interpretation): EvaluateStep =
|
||||
EvaluateStepImpl(
|
||||
TagInterpreter.new(interpretation),
|
||||
storage.tracker,
|
||||
PlaylistInterpreter.new(interpretation),
|
||||
storage.storedPlaylists,
|
||||
LibraryFactory.new())
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private class EvaluateStepImpl(
|
||||
private val tagInterpreter: TagInterpreter,
|
||||
private val tracker: Tracker,
|
||||
private val playlistInterpreter: PlaylistInterpreter,
|
||||
private val storedPlaylists: StoredPlaylists,
|
||||
private val libraryFactory: LibraryFactory
|
||||
|
@ -69,6 +75,13 @@ private class EvaluateStepImpl(
|
|||
.map { wrap(it, tagInterpreter::interpret) }
|
||||
.flowOn(Dispatchers.Default)
|
||||
.buffer(Channel.UNLIMITED)
|
||||
val trackDistributedFlow = preSongs.distribute(8)
|
||||
val trackedSongs =
|
||||
merge(
|
||||
trackDistributedFlow.manager,
|
||||
trackDistributedFlow.flows
|
||||
.map { flow -> flow.map { wrap(it, tracker::track) } }
|
||||
.flattenMerge())
|
||||
val prePlaylists =
|
||||
filterFlow.left
|
||||
.map { wrap(it, playlistInterpreter::interpret) }
|
||||
|
@ -78,7 +91,7 @@ private class EvaluateStepImpl(
|
|||
val graphBuild =
|
||||
merge(
|
||||
filterFlow.manager,
|
||||
preSongs.onEach { wrap(it, graphBuilder::add) },
|
||||
trackedSongs.onEach { wrap(it, graphBuilder::add) },
|
||||
prePlaylists.onEach { wrap(it, graphBuilder::add) })
|
||||
graphBuild.collect()
|
||||
val graph = graphBuilder.build()
|
||||
|
|
|
@ -22,6 +22,7 @@ import org.oxycblt.musikr.fs.DeviceFile
|
|||
import org.oxycblt.musikr.playlist.PlaylistFile
|
||||
import org.oxycblt.musikr.playlist.interpret.PrePlaylist
|
||||
import org.oxycblt.musikr.tag.interpret.PreSong
|
||||
import org.oxycblt.musikr.track.TrackedSong
|
||||
|
||||
class PipelineException(val processing: WhileProcessing, val error: Exception) : Exception() {
|
||||
override val cause = error
|
||||
|
@ -46,6 +47,11 @@ sealed interface WhileProcessing {
|
|||
override fun toString() = "Pre Song @ ${preSong.path}"
|
||||
}
|
||||
|
||||
class ATrackedSong internal constructor(private val trackedSong: TrackedSong) :
|
||||
WhileProcessing {
|
||||
override fun toString() = "Tracked Song @ ${trackedSong.preSong.path}"
|
||||
}
|
||||
|
||||
class APrePlaylist internal constructor(private val prePlaylist: PrePlaylist) :
|
||||
WhileProcessing {
|
||||
override fun toString() = "Pre Playlist @ ${prePlaylist.name}"
|
||||
|
@ -80,6 +86,13 @@ internal suspend fun <R> wrap(song: PreSong, block: suspend (PreSong) -> R): R =
|
|||
throw PipelineException(WhileProcessing.APreSong(song), e)
|
||||
}
|
||||
|
||||
internal suspend fun <R> wrap(song: TrackedSong, block: suspend (TrackedSong) -> R): R =
|
||||
try {
|
||||
block(song)
|
||||
} catch (e: Exception) {
|
||||
throw PipelineException(WhileProcessing.ATrackedSong(song), e)
|
||||
}
|
||||
|
||||
internal suspend fun <R> wrap(playlist: PrePlaylist, block: suspend (PrePlaylist) -> R): R =
|
||||
try {
|
||||
block(playlist)
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
package org.oxycblt.musikr.tag.interpret
|
||||
|
||||
import android.net.Uri
|
||||
|
@ -47,27 +47,27 @@ internal data class PreSong(
|
|||
val sampleRateHz: Int,
|
||||
val replayGainAdjustment: ReplayGainAdjustment,
|
||||
val lastModified: Long,
|
||||
val dateAdded: Long,
|
||||
val cover: Cover?,
|
||||
val preAlbum: PreAlbum,
|
||||
val preArtists: List<PreArtist>,
|
||||
val preGenres: List<PreGenre>
|
||||
) {
|
||||
val uid = musicBrainzId?.let { Music.UID.musicBrainz(Music.UID.Item.SONG, it) }
|
||||
?: Music.UID.auxio(Music.UID.Item.SONG) {
|
||||
// Song UIDs are based on the raw data without parsing so that they remain
|
||||
// consistent across music setting changes. Parents are not held up to the
|
||||
// same standard since grouping is already inherently linked to settings.
|
||||
update(rawName)
|
||||
update(preAlbum.rawName)
|
||||
update(date)
|
||||
val uid =
|
||||
musicBrainzId?.let { Music.UID.musicBrainz(Music.UID.Item.SONG, it) }
|
||||
?: Music.UID.auxio(Music.UID.Item.SONG) {
|
||||
// Song UIDs are based on the raw data without parsing so that they remain
|
||||
// consistent across music setting changes. Parents are not held up to the
|
||||
// same standard since grouping is already inherently linked to settings.
|
||||
update(rawName)
|
||||
update(preAlbum.rawName)
|
||||
update(date)
|
||||
|
||||
update(track)
|
||||
update(disc?.number)
|
||||
update(track)
|
||||
update(disc?.number)
|
||||
|
||||
update(preArtists.map { artist -> artist.rawName })
|
||||
update(preAlbum.preArtists.map { artist -> artist.rawName })
|
||||
}
|
||||
update(preArtists.map { artist -> artist.rawName })
|
||||
update(preAlbum.preArtists.map { artist -> artist.rawName })
|
||||
}
|
||||
}
|
||||
|
||||
internal data class PreAlbum(
|
||||
|
|
|
@ -65,8 +65,6 @@ private class TagInterpreterImpl(private val interpretation: Interpretation) : T
|
|||
size = song.file.size,
|
||||
format = Format.infer(song.file.mimeType, song.properties.mimeType),
|
||||
lastModified = song.file.lastModified,
|
||||
// TODO: Figure out what to do with date added
|
||||
dateAdded = song.file.lastModified,
|
||||
musicBrainzId = song.tags.musicBrainzId?.toUuidOrNull(),
|
||||
name = interpretation.naming.name(song.tags.name, song.tags.sortName),
|
||||
rawName = song.tags.name,
|
||||
|
|
43
musikr/src/main/java/org/oxycblt/musikr/track/Tracker.kt
Normal file
43
musikr/src/main/java/org/oxycblt/musikr/track/Tracker.kt
Normal file
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* Tracker.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.musikr.track
|
||||
|
||||
import android.content.Context
|
||||
import org.oxycblt.musikr.tag.interpret.PreSong
|
||||
|
||||
abstract class Tracker {
|
||||
internal abstract suspend fun track(preSong: PreSong): TrackedSong
|
||||
|
||||
companion object {
|
||||
fun from(context: Context): Tracker =
|
||||
TrackerImpl(TrackerDatabase.from(context).trackedSongsDao())
|
||||
}
|
||||
}
|
||||
|
||||
internal data class TrackedSong(val preSong: PreSong, val dateAdded: Long)
|
||||
|
||||
private class TrackerImpl(private val dao: TrackedSongsDao) : Tracker() {
|
||||
override suspend fun track(preSong: PreSong): TrackedSong {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
val entity = TrackedSongEntity(uid = preSong.uid.toString(), dateAdded = currentTime)
|
||||
dao.insertSong(entity)
|
||||
val trackedEntity = dao.selectSong(preSong.uid.toString())
|
||||
return TrackedSong(preSong = preSong, dateAdded = trackedEntity?.dateAdded ?: currentTime)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* TrackerDatabase.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.musikr.track
|
||||
|
||||
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
|
||||
|
||||
@Database(entities = [TrackedSongEntity::class], version = 1, exportSchema = false)
|
||||
internal abstract class TrackerDatabase : RoomDatabase() {
|
||||
abstract fun trackedSongsDao(): TrackedSongsDao
|
||||
|
||||
companion object {
|
||||
fun from(context: Context) =
|
||||
Room.databaseBuilder(
|
||||
context.applicationContext, TrackerDatabase::class.java, "tracked_songs.db")
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
@Entity internal data class TrackedSongEntity(@PrimaryKey val uid: String, val dateAdded: Long)
|
||||
|
||||
@Dao
|
||||
internal interface TrackedSongsDao {
|
||||
@Query("SELECT * FROM TrackedSongEntity WHERE uid = :uid")
|
||||
suspend fun selectSong(uid: String): TrackedSongEntity?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertSong(trackedSong: TrackedSongEntity)
|
||||
}
|
Loading…
Reference in a new issue