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

View file

@ -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 =

View file

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

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.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>

View file

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

View file

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

View file

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

View file

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

View file

@ -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(

View file

@ -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,

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