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:
Alexander Capehart 2024-12-24 15:23:33 -05:00
parent c42ac644eb
commit ca6388b28d
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
12 changed files with 158 additions and 29 deletions

View file

@ -28,6 +28,7 @@ import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton import javax.inject.Singleton
import org.oxycblt.musikr.cache.Cache import org.oxycblt.musikr.cache.Cache
import org.oxycblt.musikr.playlist.db.StoredPlaylists import org.oxycblt.musikr.playlist.db.StoredPlaylists
import org.oxycblt.musikr.track.Tracker
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
@ -42,6 +43,8 @@ interface MusicModule {
class MusikrShimModule { class MusikrShimModule {
@Singleton @Provides fun cache(@ApplicationContext context: Context) = Cache.from(context) @Singleton @Provides fun cache(@ApplicationContext context: Context) = Cache.from(context)
@Singleton @Provides fun tracker(@ApplicationContext context: Context) = Tracker.from(context)
@Singleton @Singleton
@Provides @Provides
fun storedPlaylists(@ApplicationContext context: Context) = StoredPlaylists.from(context) fun storedPlaylists(@ApplicationContext context: Context) = StoredPlaylists.from(context)

View file

@ -42,6 +42,7 @@ import org.oxycblt.musikr.cache.WriteOnlyCache
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
import org.oxycblt.musikr.tag.interpret.Separators import org.oxycblt.musikr.tag.interpret.Separators
import org.oxycblt.musikr.track.Tracker
import timber.log.Timber as L import timber.log.Timber as L
/** /**
@ -215,6 +216,7 @@ class MusicRepositoryImpl
constructor( constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val cache: Cache, private val cache: Cache,
private val tracker: Tracker,
private val storedPlaylists: StoredPlaylists, private val storedPlaylists: StoredPlaylists,
private val musicSettings: MusicSettings private val musicSettings: MusicSettings
) : MusicRepository { ) : MusicRepository {
@ -365,7 +367,7 @@ constructor(
val newRevision = currentRevision?.takeIf { withCache } ?: UUID.randomUUID() val newRevision = currentRevision?.takeIf { withCache } ?: UUID.randomUUID()
val cache = if (withCache) cache else WriteOnlyCache(cache) val cache = if (withCache) cache else WriteOnlyCache(cache)
val covers = MutableRevisionedStoredCovers(context, newRevision) val covers = MutableRevisionedStoredCovers(context, newRevision)
val storage = Storage(cache, covers, storedPlaylists) val storage = Storage(cache, tracker, covers, storedPlaylists)
val interpretation = Interpretation(nameFactory, separators) val interpretation = Interpretation(nameFactory, separators)
val newLibrary = val newLibrary =

View file

@ -23,9 +23,11 @@ import org.oxycblt.musikr.cover.MutableStoredCovers
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
import org.oxycblt.musikr.tag.interpret.Separators import org.oxycblt.musikr.tag.interpret.Separators
import org.oxycblt.musikr.track.Tracker
data class Storage( data class Storage(
val cache: Cache, val cache: Cache,
val tracker: Tracker,
val storedCovers: MutableStoredCovers, val storedCovers: MutableStoredCovers,
val storedPlaylists: StoredPlaylists val storedPlaylists: StoredPlaylists
) )

View file

@ -24,7 +24,7 @@ import org.oxycblt.musikr.playlist.interpret.PrePlaylist
import org.oxycblt.musikr.tag.interpret.PreAlbum import org.oxycblt.musikr.tag.interpret.PreAlbum
import org.oxycblt.musikr.tag.interpret.PreArtist import org.oxycblt.musikr.tag.interpret.PreArtist
import org.oxycblt.musikr.tag.interpret.PreGenre 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 import org.oxycblt.musikr.util.unlikelyToBeNull
internal data class MusicGraph( internal data class MusicGraph(
@ -35,7 +35,7 @@ internal data class MusicGraph(
val playlistVertex: Set<PlaylistVertex> val playlistVertex: Set<PlaylistVertex>
) { ) {
interface Builder { interface Builder {
fun add(preSong: PreSong) fun add(trackedSong: TrackedSong)
fun add(prePlaylist: PrePlaylist) fun add(prePlaylist: PrePlaylist)
@ -54,7 +54,8 @@ private class MusicGraphBuilderImpl : MusicGraph.Builder {
private val genreVertices = mutableMapOf<PreGenre, GenreVertex>() private val genreVertices = mutableMapOf<PreGenre, GenreVertex>()
private val playlistVertices = mutableSetOf<PlaylistVertex>() private val playlistVertices = mutableSetOf<PlaylistVertex>()
override fun add(preSong: PreSong) { override fun add(trackedSong: TrackedSong) {
val preSong = trackedSong.preSong
val uid = preSong.uid val uid = preSong.uid
if (songVertices.containsKey(uid)) { if (songVertices.containsKey(uid)) {
return return
@ -88,7 +89,7 @@ private class MusicGraphBuilderImpl : MusicGraph.Builder {
val songVertex = val songVertex =
SongVertex( SongVertex(
preSong, trackedSong,
albumVertex, albumVertex,
songArtistVertices.toMutableList(), songArtistVertices.toMutableList(),
songGenreVertices.toMutableList()) songGenreVertices.toMutableList())
@ -311,7 +312,7 @@ private class MusicGraphBuilderImpl : MusicGraph.Builder {
} }
internal class SongVertex( internal class SongVertex(
val preSong: PreSong, val trackedSong: TrackedSong,
var albumVertex: AlbumVertex, var albumVertex: AlbumVertex,
var artistVertices: MutableList<ArtistVertex>, var artistVertices: MutableList<ArtistVertex>,
var genreVertices: MutableList<GenreVertex> var genreVertices: MutableList<GenreVertex>

View file

@ -75,7 +75,7 @@ private class LibraryFactoryImpl() : LibraryFactory {
} }
private class SongVertexCore(private val vertex: SongVertex) : SongCore { 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 override fun resolveAlbum() = vertex.albumVertex.tag as Album

View file

@ -22,10 +22,10 @@ import org.oxycblt.musikr.Album
import org.oxycblt.musikr.Artist import org.oxycblt.musikr.Artist
import org.oxycblt.musikr.Genre import org.oxycblt.musikr.Genre
import org.oxycblt.musikr.Song import org.oxycblt.musikr.Song
import org.oxycblt.musikr.tag.interpret.PreSong import org.oxycblt.musikr.track.TrackedSong
internal interface SongCore { internal interface SongCore {
val preSong: PreSong val trackedSong: TrackedSong
fun resolveAlbum(): Album fun resolveAlbum(): Album
@ -40,7 +40,7 @@ internal interface SongCore {
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
internal class SongImpl(private val handle: SongCore) : Song { 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 uid = preSong.uid
override val name = preSong.name override val name = preSong.name
@ -56,7 +56,7 @@ internal class SongImpl(private val handle: SongCore) : Song {
override val sampleRateHz = preSong.sampleRateHz override val sampleRateHz = preSong.sampleRateHz
override val replayGainAdjustment = preSong.replayGainAdjustment override val replayGainAdjustment = preSong.replayGainAdjustment
override val lastModified = preSong.lastModified override val lastModified = preSong.lastModified
override val dateAdded = preSong.dateAdded override val dateAdded = handle.trackedSong.dateAdded
override val cover = preSong.cover override val cover = preSong.cover
override val album: Album override val album: Album
get() = handle.resolveAlbum() get() = handle.resolveAlbum()

View file

@ -19,10 +19,12 @@
package org.oxycblt.musikr.pipeline package org.oxycblt.musikr.pipeline
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flattenMerge
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge 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.db.StoredPlaylists
import org.oxycblt.musikr.playlist.interpret.PlaylistInterpreter import org.oxycblt.musikr.playlist.interpret.PlaylistInterpreter
import org.oxycblt.musikr.tag.interpret.TagInterpreter import org.oxycblt.musikr.tag.interpret.TagInterpreter
import org.oxycblt.musikr.track.Tracker
internal interface EvaluateStep { internal interface EvaluateStep {
suspend fun evaluate(extractedMusic: Flow<ExtractedMusic>): MutableLibrary suspend fun evaluate(extractedMusic: Flow<ExtractedMusic>): MutableLibrary
@ -43,14 +46,17 @@ internal interface EvaluateStep {
fun new(storage: Storage, interpretation: Interpretation): EvaluateStep = fun new(storage: Storage, interpretation: Interpretation): EvaluateStep =
EvaluateStepImpl( EvaluateStepImpl(
TagInterpreter.new(interpretation), TagInterpreter.new(interpretation),
storage.tracker,
PlaylistInterpreter.new(interpretation), PlaylistInterpreter.new(interpretation),
storage.storedPlaylists, storage.storedPlaylists,
LibraryFactory.new()) LibraryFactory.new())
} }
} }
@OptIn(ExperimentalCoroutinesApi::class)
private class EvaluateStepImpl( private class EvaluateStepImpl(
private val tagInterpreter: TagInterpreter, private val tagInterpreter: TagInterpreter,
private val tracker: Tracker,
private val playlistInterpreter: PlaylistInterpreter, private val playlistInterpreter: PlaylistInterpreter,
private val storedPlaylists: StoredPlaylists, private val storedPlaylists: StoredPlaylists,
private val libraryFactory: LibraryFactory private val libraryFactory: LibraryFactory
@ -69,6 +75,13 @@ private class EvaluateStepImpl(
.map { wrap(it, tagInterpreter::interpret) } .map { wrap(it, tagInterpreter::interpret) }
.flowOn(Dispatchers.Default) .flowOn(Dispatchers.Default)
.buffer(Channel.UNLIMITED) .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 = val prePlaylists =
filterFlow.left filterFlow.left
.map { wrap(it, playlistInterpreter::interpret) } .map { wrap(it, playlistInterpreter::interpret) }
@ -78,7 +91,7 @@ private class EvaluateStepImpl(
val graphBuild = val graphBuild =
merge( merge(
filterFlow.manager, filterFlow.manager,
preSongs.onEach { wrap(it, graphBuilder::add) }, trackedSongs.onEach { 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()

View file

@ -22,6 +22,7 @@ import org.oxycblt.musikr.fs.DeviceFile
import org.oxycblt.musikr.playlist.PlaylistFile import org.oxycblt.musikr.playlist.PlaylistFile
import org.oxycblt.musikr.playlist.interpret.PrePlaylist import org.oxycblt.musikr.playlist.interpret.PrePlaylist
import org.oxycblt.musikr.tag.interpret.PreSong import org.oxycblt.musikr.tag.interpret.PreSong
import org.oxycblt.musikr.track.TrackedSong
class PipelineException(val processing: WhileProcessing, val error: Exception) : Exception() { class PipelineException(val processing: WhileProcessing, val error: Exception) : Exception() {
override val cause = error override val cause = error
@ -46,6 +47,11 @@ sealed interface WhileProcessing {
override fun toString() = "Pre Song @ ${preSong.path}" 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) : class APrePlaylist internal constructor(private val prePlaylist: PrePlaylist) :
WhileProcessing { WhileProcessing {
override fun toString() = "Pre Playlist @ ${prePlaylist.name}" 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) 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 = internal suspend fun <R> wrap(playlist: PrePlaylist, block: suspend (PrePlaylist) -> R): R =
try { try {
block(playlist) block(playlist)

View file

@ -15,7 +15,7 @@
* 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.tag.interpret package org.oxycblt.musikr.tag.interpret
import android.net.Uri import android.net.Uri
@ -47,27 +47,27 @@ internal data class PreSong(
val sampleRateHz: Int, val sampleRateHz: Int,
val replayGainAdjustment: ReplayGainAdjustment, val replayGainAdjustment: ReplayGainAdjustment,
val lastModified: Long, val lastModified: Long,
val dateAdded: Long,
val cover: Cover?, val cover: Cover?,
val preAlbum: PreAlbum, val preAlbum: PreAlbum,
val preArtists: List<PreArtist>, val preArtists: List<PreArtist>,
val preGenres: List<PreGenre> val preGenres: List<PreGenre>
) { ) {
val uid = musicBrainzId?.let { Music.UID.musicBrainz(Music.UID.Item.SONG, it) } val uid =
?: Music.UID.auxio(Music.UID.Item.SONG) { musicBrainzId?.let { Music.UID.musicBrainz(Music.UID.Item.SONG, it) }
// Song UIDs are based on the raw data without parsing so that they remain ?: Music.UID.auxio(Music.UID.Item.SONG) {
// consistent across music setting changes. Parents are not held up to the // Song UIDs are based on the raw data without parsing so that they remain
// same standard since grouping is already inherently linked to settings. // consistent across music setting changes. Parents are not held up to the
update(rawName) // same standard since grouping is already inherently linked to settings.
update(preAlbum.rawName) update(rawName)
update(date) update(preAlbum.rawName)
update(date)
update(track) update(track)
update(disc?.number) update(disc?.number)
update(preArtists.map { artist -> artist.rawName }) update(preArtists.map { artist -> artist.rawName })
update(preAlbum.preArtists.map { artist -> artist.rawName }) update(preAlbum.preArtists.map { artist -> artist.rawName })
} }
} }
internal data class PreAlbum( internal data class PreAlbum(

View file

@ -65,8 +65,6 @@ private class TagInterpreterImpl(private val interpretation: Interpretation) : T
size = song.file.size, size = song.file.size,
format = Format.infer(song.file.mimeType, song.properties.mimeType), format = Format.infer(song.file.mimeType, song.properties.mimeType),
lastModified = song.file.lastModified, lastModified = song.file.lastModified,
// TODO: Figure out what to do with date added
dateAdded = song.file.lastModified,
musicBrainzId = song.tags.musicBrainzId?.toUuidOrNull(), musicBrainzId = song.tags.musicBrainzId?.toUuidOrNull(),
name = interpretation.naming.name(song.tags.name, song.tags.sortName), name = interpretation.naming.name(song.tags.name, song.tags.sortName),
rawName = song.tags.name, rawName = song.tags.name,

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

View file

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