diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicModule.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicModule.kt index c276ca414..37eb96878 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicModule.kt @@ -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) 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 159a31ca8..5f1e1fb4a 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -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 = diff --git a/musikr/src/main/java/org/oxycblt/musikr/Config.kt b/musikr/src/main/java/org/oxycblt/musikr/Config.kt index 22ec833dd..7158f9446 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/Config.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/Config.kt @@ -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 ) diff --git a/musikr/src/main/java/org/oxycblt/musikr/graph/MusicGraph.kt b/musikr/src/main/java/org/oxycblt/musikr/graph/MusicGraph.kt index d08a979ce..21e0f12f9 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/graph/MusicGraph.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/graph/MusicGraph.kt @@ -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 ) { 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() private val playlistVertices = mutableSetOf() - 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, var genreVertices: MutableList diff --git a/musikr/src/main/java/org/oxycblt/musikr/model/LibraryFactory.kt b/musikr/src/main/java/org/oxycblt/musikr/model/LibraryFactory.kt index bffe06b0c..d3ed34b53 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/model/LibraryFactory.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/model/LibraryFactory.kt @@ -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 diff --git a/musikr/src/main/java/org/oxycblt/musikr/model/SongImpl.kt b/musikr/src/main/java/org/oxycblt/musikr/model/SongImpl.kt index 2747edd1d..b77e90195 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/model/SongImpl.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/model/SongImpl.kt @@ -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() 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 2ccc992be..65b94b4e4 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/EvaluateStep.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/EvaluateStep.kt @@ -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): 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() 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 1f6efc892..8a2525fc8 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/PipelineException.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/PipelineException.kt @@ -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 wrap(song: PreSong, block: suspend (PreSong) -> R): R = throw PipelineException(WhileProcessing.APreSong(song), e) } +internal suspend fun wrap(song: TrackedSong, block: suspend (TrackedSong) -> R): R = + try { + block(song) + } catch (e: Exception) { + throw PipelineException(WhileProcessing.ATrackedSong(song), e) + } + internal suspend fun wrap(playlist: PrePlaylist, block: suspend (PrePlaylist) -> R): R = try { block(playlist) 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 ba6c28da4..ae109e556 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 @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - + 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, val preGenres: List ) { - 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( 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 b6f07df30..b9b4b297b 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 @@ -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, diff --git a/musikr/src/main/java/org/oxycblt/musikr/track/Tracker.kt b/musikr/src/main/java/org/oxycblt/musikr/track/Tracker.kt new file mode 100644 index 000000000..acf5e0aad --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/track/Tracker.kt @@ -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 . + */ + +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) + } +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/track/TrackerDatabase.kt b/musikr/src/main/java/org/oxycblt/musikr/track/TrackerDatabase.kt new file mode 100644 index 000000000..e30676141 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/track/TrackerDatabase.kt @@ -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 . + */ + +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) +}