From e96b81c62a57882a82f8e16e65698ba559a02ecd Mon Sep 17 00:00:00 2001 From: Zyrouge Date: Sun, 19 Jan 2025 23:14:11 +0530 Subject: [PATCH 01/15] refactor: add partial model classes --- .../2.json | 60 ++- .../1.json | 8 +- .../services/database/CacheDatabase.kt | 40 -- .../symphony/services/database/Database.kt | 42 +- .../services/database/PersistentDatabase.kt | 70 +++- .../adapters/SQLiteKeyValueDatabaseAdapter.kt | 127 ------ .../database/store/AlbumArtistMappingStore.kt | 11 + .../database/store/AlbumSongMappingStore.kt | 11 + .../services/database/store/AlbumStore.kt | 23 ++ .../database/store/ArtistSongMappingStore.kt | 11 + .../services/database/store/ArtistStore.kt | 23 ++ .../database/store/ArtworkIndexStore.kt | 17 + .../{ArtworkCacheStore.kt => ArtworkStore.kt} | 10 +- .../database/store/GenreSongMappingStore.kt | 11 + .../services/database/store/GenreStore.kt | 23 ++ .../database/store/LyricsCacheStore.kt | 19 - .../database/store/MediaTreeFolderStore.kt | 29 ++ .../store/MediaTreeLyricsFileStore.kt | 26 ++ .../database/store/MediaTreeSongFileStore.kt | 27 ++ .../store/PlaylistSongMappingStore.kt | 16 + .../services/database/store/PlaylistStore.kt | 19 +- .../services/database/store/SongCacheStore.kt | 29 -- .../services/database/store/SongFileStore.kt | 29 ++ .../database/store/SongLyricsStore.kt | 12 + .../services/database/store/SongStore.kt | 29 ++ .../zyrouge/symphony/services/groove/Album.kt | 26 -- .../symphony/services/groove/AlbumArtist.kt | 23 -- .../zyrouge/symphony/services/groove/Genre.kt | 17 - .../symphony/services/groove/MediaExposer.kt | 361 +++++++++++------- .../services/groove/entities/Album.kt | 43 +++ .../groove/entities/AlbumArtistMapping.kt | 46 +++ .../groove/entities/AlbumSongMapping.kt | 39 ++ .../services/groove/{ => entities}/Artist.kt | 22 +- .../groove/entities/ArtistSongMapping.kt | 39 ++ .../services/groove/entities/ArtworkIndex.kt | 35 ++ .../services/groove/entities/Genre.kt | 34 ++ .../groove/entities/GenreSongMapping.kt | 39 ++ .../groove/entities/MediaTreeFolder.kt | 52 +++ .../groove/entities/MediaTreeLyricsFile.kt | 54 +++ .../groove/entities/MediaTreeSongFile.kt | 57 +++ .../groove/{ => entities}/Playlist.kt | 19 +- .../groove/entities/PlaylistSongMapping.kt | 60 +++ .../symphony/services/groove/entities/Song.kt | 127 ++++++ .../groove/{Song.kt => entities/SongFile.kt} | 174 ++++----- .../services/groove/entities/SongLyrics.kt | 33 ++ .../groove/repositories/SongRepository.kt | 19 +- .../zyrouge/symphony/utils/KeyGenerator.kt | 20 +- .../io/github/zyrouge/symphony/utils/Set.kt | 2 +- .../symphony/utils/SimpleFileSystem.kt | 47 +-- 49 files changed, 1480 insertions(+), 630 deletions(-) delete mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/database/CacheDatabase.kt delete mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/database/adapters/SQLiteKeyValueDatabaseAdapter.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumArtistMappingStore.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumSongMappingStore.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumStore.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistSongMappingStore.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistStore.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtworkIndexStore.kt rename app/src/main/java/io/github/zyrouge/symphony/services/database/store/{ArtworkCacheStore.kt => ArtworkStore.kt} (53%) create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreSongMappingStore.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreStore.kt delete mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/database/store/LyricsCacheStore.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeFolderStore.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeLyricsFileStore.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeSongFileStore.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistSongMappingStore.kt delete mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongCacheStore.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongFileStore.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongLyricsStore.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongStore.kt delete mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/groove/Album.kt delete mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/groove/AlbumArtist.kt delete mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/groove/Genre.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Album.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/AlbumArtistMapping.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/AlbumSongMapping.kt rename app/src/main/java/io/github/zyrouge/symphony/services/groove/{ => entities}/Artist.kt (57%) create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/ArtistSongMapping.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/ArtworkIndex.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Genre.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/GenreSongMapping.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/MediaTreeFolder.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/MediaTreeLyricsFile.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/MediaTreeSongFile.kt rename app/src/main/java/io/github/zyrouge/symphony/services/groove/{ => entities}/Playlist.kt (85%) create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/PlaylistSongMapping.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Song.kt rename app/src/main/java/io/github/zyrouge/symphony/services/groove/{Song.kt => entities/SongFile.kt} (66%) create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongLyrics.kt diff --git a/app/room-schemas/io.github.zyrouge.symphony.services.database.CacheDatabase/2.json b/app/room-schemas/io.github.zyrouge.symphony.services.database.CacheDatabase/2.json index 9fb5d4e4..2547ecc4 100644 --- a/app/room-schemas/io.github.zyrouge.symphony.services.database.CacheDatabase/2.json +++ b/app/room-schemas/io.github.zyrouge.symphony.services.database.CacheDatabase/2.json @@ -2,11 +2,11 @@ "formatVersion": 1, "database": { "version": 2, - "identityHash": "03822e08bb41204b4a0237835b616d5b", + "identityHash": "62b63bbc48d6e0426718cdc16623be30", "entities": [ { "tableName": "songs", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `album` TEXT, `artists` TEXT NOT NULL, `composers` TEXT NOT NULL, `albumArtists` TEXT NOT NULL, `genres` TEXT NOT NULL, `trackNumber` INTEGER, `trackTotal` INTEGER, `discNumber` INTEGER, `discTotal` INTEGER, `date` TEXT, `year` INTEGER, `duration` INTEGER NOT NULL, `bitrate` INTEGER, `samplingRate` INTEGER, `channels` INTEGER, `encoder` TEXT, `dateModified` INTEGER NOT NULL, `size` INTEGER NOT NULL, `coverFile` TEXT, `uri` TEXT NOT NULL, `path` TEXT NOT NULL, PRIMARY KEY(`id`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `album` TEXT, `track_number` INTEGER, `track_total` INTEGER, `disc_number` INTEGER, `disc_total` INTEGER, `date` TEXT, `year` INTEGER, `duration` INTEGER NOT NULL, `bitrate` INTEGER, `sampling_rate` INTEGER, `channels` INTEGER, `encoder` TEXT, `date_modified` INTEGER NOT NULL, `size` INTEGER NOT NULL, `cover_file` TEXT, `uri` TEXT NOT NULL, `path` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `song_files`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "id", @@ -21,56 +21,32 @@ "notNull": true }, { - "fieldPath": "album", + "fieldPath": "albumId", "columnName": "album", "affinity": "TEXT", "notNull": false }, - { - "fieldPath": "artists", - "columnName": "artists", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "composers", - "columnName": "composers", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "albumArtists", - "columnName": "albumArtists", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "genres", - "columnName": "genres", - "affinity": "TEXT", - "notNull": true - }, { "fieldPath": "trackNumber", - "columnName": "trackNumber", + "columnName": "track_number", "affinity": "INTEGER", "notNull": false }, { "fieldPath": "trackTotal", - "columnName": "trackTotal", + "columnName": "track_total", "affinity": "INTEGER", "notNull": false }, { "fieldPath": "discNumber", - "columnName": "discNumber", + "columnName": "disc_number", "affinity": "INTEGER", "notNull": false }, { "fieldPath": "discTotal", - "columnName": "discTotal", + "columnName": "disc_total", "affinity": "INTEGER", "notNull": false }, @@ -100,7 +76,7 @@ }, { "fieldPath": "samplingRate", - "columnName": "samplingRate", + "columnName": "sampling_rate", "affinity": "INTEGER", "notNull": false }, @@ -118,7 +94,7 @@ }, { "fieldPath": "dateModified", - "columnName": "dateModified", + "columnName": "date_modified", "affinity": "INTEGER", "notNull": true }, @@ -130,7 +106,7 @@ }, { "fieldPath": "coverFile", - "columnName": "coverFile", + "columnName": "cover_file", "affinity": "TEXT", "notNull": false }, @@ -154,13 +130,25 @@ ] }, "indices": [], - "foreignKeys": [] + "foreignKeys": [ + { + "table": "song_files", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] } ], "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '03822e08bb41204b4a0237835b616d5b')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '62b63bbc48d6e0426718cdc16623be30')" ] } } \ No newline at end of file diff --git a/app/room-schemas/io.github.zyrouge.symphony.services.database.PersistentDatabase/1.json b/app/room-schemas/io.github.zyrouge.symphony.services.database.PersistentDatabase/1.json index c8c278c3..438b4b3f 100644 --- a/app/room-schemas/io.github.zyrouge.symphony.services.database.PersistentDatabase/1.json +++ b/app/room-schemas/io.github.zyrouge.symphony.services.database.PersistentDatabase/1.json @@ -2,11 +2,11 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "6091615e6ae35543e127d6744215fad8", + "identityHash": "911e8d9ac1e9715396e1465009d548b3", "entities": [ { "tableName": "playlists", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `songPaths` TEXT NOT NULL, `uri` TEXT, `path` TEXT, PRIMARY KEY(`id`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `songPaths` TEXT NOT NULL, `title` TEXT, `path` TEXT, PRIMARY KEY(`id`))", "fields": [ { "fieldPath": "id", @@ -28,7 +28,7 @@ }, { "fieldPath": "uri", - "columnName": "uri", + "columnName": "title", "affinity": "TEXT", "notNull": false }, @@ -52,7 +52,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6091615e6ae35543e127d6744215fad8')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '911e8d9ac1e9715396e1465009d548b3')" ] } } \ No newline at end of file diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/CacheDatabase.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/CacheDatabase.kt deleted file mode 100644 index 973d4949..00000000 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/CacheDatabase.kt +++ /dev/null @@ -1,40 +0,0 @@ -package io.github.zyrouge.symphony.services.database - -import androidx.room.AutoMigration -import androidx.room.Database -import androidx.room.DeleteColumn -import androidx.room.Room -import androidx.room.RoomDatabase -import androidx.room.TypeConverters -import androidx.room.migration.AutoMigrationSpec -import io.github.zyrouge.symphony.Symphony -import io.github.zyrouge.symphony.services.database.store.SongCacheStore -import io.github.zyrouge.symphony.services.groove.Song -import io.github.zyrouge.symphony.utils.RoomConvertors - -@Database( - entities = [Song::class], - version = 2, - autoMigrations = [AutoMigration(1, 2, CacheDatabase.Migration1To2::class)] -) -@TypeConverters(RoomConvertors::class) -abstract class CacheDatabase : RoomDatabase() { - abstract fun songs(): SongCacheStore - - companion object { - fun create(symphony: Symphony) = Room - .databaseBuilder( - symphony.applicationContext, - CacheDatabase::class.java, - "cache" - ) - .build() - } - - @DeleteColumn("songs", "minBitrate") - @DeleteColumn("songs", "maxBitrate") - @DeleteColumn("songs", "bitsPerSample") - @DeleteColumn("songs", "samples") - @DeleteColumn("songs", "codec") - class Migration1To2 : AutoMigrationSpec -} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/Database.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/Database.kt index 71c9a588..2657eb0f 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/Database.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/Database.kt @@ -1,15 +1,45 @@ package io.github.zyrouge.symphony.services.database import io.github.zyrouge.symphony.Symphony -import io.github.zyrouge.symphony.services.database.store.ArtworkCacheStore -import io.github.zyrouge.symphony.services.database.store.LyricsCacheStore +import io.github.zyrouge.symphony.services.database.store.ArtworkStore +import io.github.zyrouge.symphony.utils.KeyGenerator class Database(symphony: Symphony) { - private val cache = CacheDatabase.create(symphony) private val persistent = PersistentDatabase.create(symphony) - val artworkCache = ArtworkCacheStore(symphony) - val lyricsCache = LyricsCacheStore(symphony) - val songCache get() = cache.songs() + val albumArtistSongsIdGenerator = KeyGenerator.TimeIncremental() + val albumArtistsIdGenerator = KeyGenerator.TimeIncremental() + val albumSongsIdGenerator = KeyGenerator.TimeIncremental() + val albumsIdGenerator = KeyGenerator.TimeIncremental() + val artistSongsIdGenerator = KeyGenerator.TimeIncremental() + val artistsIdGenerator = KeyGenerator.TimeIncremental() + val mediaTreeFoldersIdGenerator = KeyGenerator.TimeIncremental() + val mediaTreeSongFilesIdGenerator = KeyGenerator.TimeIncremental() + val mediaTreeLyricsFilesIdGenerator = KeyGenerator.TimeIncremental() + val genreSongsIdGenerator = KeyGenerator.TimeIncremental() + val genreIdGenerator = KeyGenerator.TimeIncremental() + val playlistSongsIdGenerator = KeyGenerator.TimeIncremental() + val playlistsIdGenerator = KeyGenerator.TimeIncremental() + val songFilesIdGenerator = KeyGenerator.TimeIncremental() + val songLyricsIdGenerator = KeyGenerator.TimeIncremental() + val songsIdGenerator = KeyGenerator.TimeIncremental() + + val albumArtistSongs get() = persistent.albumArtistSongs() + val albumArtists get() = persistent.albumArtists() + val albumSongs get() = persistent.albumSongs() + val albums get() = persistent.albums() + val artistSongs get() = persistent.artistSongs() + val artists get() = persistent.artists() + val artwork = ArtworkStore(symphony) + val artworkIndices get() = persistent.artworkIndices() + val genreSongs get() = persistent.genreSongs() + val genre get() = persistent.genre() + val mediaTreeFolders get() = persistent.mediaTreeFolders() + val mediaTreeSongFiles get() = persistent.mediaTreeSongFiles() + val mediaTreeLyricsFiles get() = persistent.mediaTreeLyricsFiles() + val playlistSongs get() = persistent.playlistSongs() val playlists get() = persistent.playlists() + val songFiles get() = persistent.songFiles() + val songLyrics get() = persistent.songLyrics() + val songs get() = persistent.songs() } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/PersistentDatabase.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/PersistentDatabase.kt index ff90262b..e22cb425 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/PersistentDatabase.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/PersistentDatabase.kt @@ -5,21 +5,85 @@ import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverters import io.github.zyrouge.symphony.Symphony +import io.github.zyrouge.symphony.services.database.store.AlbumArtistMappingStore +import io.github.zyrouge.symphony.services.database.store.AlbumSongMappingStore +import io.github.zyrouge.symphony.services.database.store.AlbumStore +import io.github.zyrouge.symphony.services.database.store.ArtistSongMappingStore +import io.github.zyrouge.symphony.services.database.store.ArtistStore +import io.github.zyrouge.symphony.services.database.store.ArtworkIndexStore +import io.github.zyrouge.symphony.services.database.store.GenreSongMappingStore +import io.github.zyrouge.symphony.services.database.store.GenreStore +import io.github.zyrouge.symphony.services.database.store.MediaTreeFolderStore +import io.github.zyrouge.symphony.services.database.store.MediaTreeLyricsFileStore +import io.github.zyrouge.symphony.services.database.store.MediaTreeSongFileStore +import io.github.zyrouge.symphony.services.database.store.PlaylistSongMappingStore import io.github.zyrouge.symphony.services.database.store.PlaylistStore -import io.github.zyrouge.symphony.services.groove.Playlist +import io.github.zyrouge.symphony.services.database.store.SongFileStore +import io.github.zyrouge.symphony.services.database.store.SongLyricsStore +import io.github.zyrouge.symphony.services.database.store.SongStore +import io.github.zyrouge.symphony.services.groove.entities.Album +import io.github.zyrouge.symphony.services.groove.entities.AlbumArtistMapping +import io.github.zyrouge.symphony.services.groove.entities.AlbumSongMapping +import io.github.zyrouge.symphony.services.groove.entities.Artist +import io.github.zyrouge.symphony.services.groove.entities.ArtistSongMapping +import io.github.zyrouge.symphony.services.groove.entities.ArtworkIndex +import io.github.zyrouge.symphony.services.groove.entities.Genre +import io.github.zyrouge.symphony.services.groove.entities.GenreSongMapping +import io.github.zyrouge.symphony.services.groove.entities.Playlist +import io.github.zyrouge.symphony.services.groove.entities.PlaylistSongMapping +import io.github.zyrouge.symphony.services.groove.entities.Song +import io.github.zyrouge.symphony.services.groove.entities.SongFile +import io.github.zyrouge.symphony.services.groove.entities.SongLyrics import io.github.zyrouge.symphony.utils.RoomConvertors -@Database(entities = [Playlist::class], version = 1) +@Database( + version = 1, + entities = [ + AlbumArtistMapping::class, + AlbumSongMapping::class, + Album::class, + ArtistSongMapping::class, + Artist::class, + ArtworkIndex::class, + GenreSongMapping::class, + Genre::class, + MediaTreeFolderStore::class, + MediaTreeLyricsFileStore::class, + MediaTreeSongFileStore::class, + PlaylistSongMapping::class, + Playlist::class, + SongFile::class, + SongLyrics::class, + Song::class, + ], +) @TypeConverters(RoomConvertors::class) abstract class PersistentDatabase : RoomDatabase() { + abstract fun albumArtistMapping(): AlbumArtistMappingStore + abstract fun albumSongMapping(): AlbumSongMappingStore + abstract fun albums(): AlbumStore + abstract fun artistSongMapping(): ArtistSongMappingStore + abstract fun artists(): ArtistStore + abstract fun artworkIndices(): ArtworkIndexStore + abstract fun genreSongMapping(): GenreSongMappingStore + abstract fun genre(): GenreStore + abstract fun mediaTreeFolders(): MediaTreeFolderStore + abstract fun mediaTreeSongFiles(): MediaTreeSongFileStore + abstract fun mediaTreeLyricsFiles(): MediaTreeLyricsFileStore + abstract fun playlistSongMapping(): PlaylistSongMappingStore abstract fun playlists(): PlaylistStore + abstract fun songFiles(): SongFileStore + abstract fun songLyrics(): SongLyricsStore + abstract fun songs(): SongStore companion object { + const val DATABASE_NAME = "symphony_persistent" + fun create(symphony: Symphony) = Room .databaseBuilder( symphony.applicationContext, PersistentDatabase::class.java, - "persistent" + DATABASE_NAME ) .build() } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/adapters/SQLiteKeyValueDatabaseAdapter.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/adapters/SQLiteKeyValueDatabaseAdapter.kt deleted file mode 100644 index ef6a279a..00000000 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/adapters/SQLiteKeyValueDatabaseAdapter.kt +++ /dev/null @@ -1,127 +0,0 @@ -package io.github.zyrouge.symphony.services.database.adapters - -import android.content.ContentValues -import android.content.Context -import android.database.sqlite.SQLiteDatabase -import android.database.sqlite.SQLiteOpenHelper - -class SQLiteKeyValueDatabaseAdapter( - private val transformer: Transformer, - private val helper: SQLiteOpenHelper, -) { - private val name: String get() = helper.databaseName - private val readableDatabase: SQLiteDatabase get() = helper.readableDatabase - private val writableDatabase: SQLiteDatabase get() = helper.writableDatabase - - fun get(key: String): T? { - val columns = arrayOf(COLUMN_VALUE) - val selection = "$COLUMN_KEY = ?" - val selectionArgs = arrayOf(key) - readableDatabase - .query(name, columns, selection, selectionArgs, null, null, null) - .use { - val valueIndex = it.getColumnIndexOrThrow(COLUMN_VALUE) - if (!it.moveToNext()) { - return null - } - val rawValue = it.getString(valueIndex) - return transformer.deserialize(rawValue) - } - } - - fun put(key: String, value: T): Boolean { - val values = ContentValues().apply { - put(COLUMN_KEY, key) - put(COLUMN_VALUE, transformer.serialize(value)) - } - val rowId = writableDatabase.insert(name, null, values) - return rowId != -1L - } - - fun delete(key: String): Boolean { - val selection = "$COLUMN_KEY = ?" - val selectionArgs = arrayOf(key) - return writableDatabase.delete(name, selection, selectionArgs) == 1 - } - - fun delete(keys: Collection): Int { - if (keys.isEmpty()) { - return 0 - } - val selectionPlaceholder = "?, ".repeat(keys.size).let { - it.substring(0, it.length - 2) - } - val selection = "$COLUMN_KEY IN (${selectionPlaceholder})" - val selectionArgs = keys.toTypedArray() - return writableDatabase.delete(name, selection, selectionArgs) - } - - fun clear() = writableDatabase.delete(name, null, null) - - fun keys(): List { - val keys = mutableListOf() - val columns = arrayOf(COLUMN_KEY) - readableDatabase - .query(name, columns, null, null, null, null, null) - .use { - val keyIndex = it.getColumnIndexOrThrow(COLUMN_KEY) - while (it.moveToNext()) { - val key = it.getString(keyIndex) - keys.add(key) - } - } - return keys - } - - fun all(): Map { - val all = mutableMapOf() - val columns = arrayOf(COLUMN_KEY, COLUMN_VALUE) - readableDatabase - .query(name, columns, null, null, null, null, null) - .use { - val keyIndex = it.getColumnIndexOrThrow(COLUMN_KEY) - val valueIndex = it.getColumnIndexOrThrow(COLUMN_VALUE) - while (it.moveToNext()) { - val key = it.getString(keyIndex) - val rawValue = it.getString(valueIndex) - val value = transformer.deserialize(rawValue) - all[key] = value - } - } - return all - } - - class CacheOpenHelper(context: Context, val name: String, version: Int) : - SQLiteOpenHelper(context, name, null, version) { - override fun onCreate(db: SQLiteDatabase) { - val query = - "CREATE TABLE $name ($COLUMN_KEY TEXT PRIMARY KEY, $COLUMN_VALUE TEXT NOT NULL)" - db.execSQL(query) - } - - override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { - val query = "DROP TABLE $name" - db.execSQL(query) - onCreate(db) - } - - override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { - onUpgrade(db, oldVersion, newVersion) - } - } - - interface Transformer { - fun serialize(data: T): String - fun deserialize(data: String): T - - class AsString : Transformer { - override fun serialize(data: String) = data - override fun deserialize(data: String) = data - } - } - - companion object { - const val COLUMN_KEY = "key" - const val COLUMN_VALUE = "value" - } -} \ No newline at end of file diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumArtistMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumArtistMappingStore.kt new file mode 100644 index 00000000..4c76136b --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumArtistMappingStore.kt @@ -0,0 +1,11 @@ +package io.github.zyrouge.symphony.services.database.store + +import androidx.room.Dao +import androidx.room.Insert +import io.github.zyrouge.symphony.services.groove.entities.AlbumArtistMapping + +@Dao +interface AlbumArtistMappingStore { + @Insert + suspend fun insert(vararg entities: AlbumArtistMapping) +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumSongMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumSongMappingStore.kt new file mode 100644 index 00000000..c0a4dbcd --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumSongMappingStore.kt @@ -0,0 +1,11 @@ +package io.github.zyrouge.symphony.services.database.store + +import androidx.room.Dao +import androidx.room.Insert +import io.github.zyrouge.symphony.services.groove.entities.AlbumSongMapping + +@Dao +interface AlbumSongMappingStore { + @Insert + suspend fun insert(vararg entities: AlbumSongMapping) +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumStore.kt new file mode 100644 index 00000000..7569ba91 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumStore.kt @@ -0,0 +1,23 @@ +package io.github.zyrouge.symphony.services.database.store + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Update +import io.github.zyrouge.symphony.services.groove.entities.Album +import kotlinx.coroutines.flow.Flow + +@Dao +interface AlbumStore { + @Insert + suspend fun insert(vararg entities: Album): List + + @Update + suspend fun update(vararg entities: Album): Int + + @Query("SELECT * FROM ${Album.TABLE}") + suspend fun values(): List + + @Query("SELECT * FROM ${Album.TABLE}") + suspend fun valuesAsFlow(): Flow> +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistSongMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistSongMappingStore.kt new file mode 100644 index 00000000..e3c5614e --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistSongMappingStore.kt @@ -0,0 +1,11 @@ +package io.github.zyrouge.symphony.services.database.store + +import androidx.room.Dao +import androidx.room.Insert +import io.github.zyrouge.symphony.services.groove.entities.ArtistSongMapping + +@Dao +interface ArtistSongMappingStore { + @Insert + suspend fun insert(vararg entities: ArtistSongMapping) +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistStore.kt new file mode 100644 index 00000000..ab3faf42 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistStore.kt @@ -0,0 +1,23 @@ +package io.github.zyrouge.symphony.services.database.store + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Update +import io.github.zyrouge.symphony.services.groove.entities.Artist +import kotlinx.coroutines.flow.Flow + +@Dao +interface ArtistStore { + @Insert + suspend fun insert(vararg entities: Artist): List + + @Update + suspend fun update(vararg entities: Artist): Int + + @Query("SELECT * FROM ${Artist.TABLE}") + suspend fun values(): List + + @Query("SELECT * FROM ${Artist.TABLE}") + suspend fun valuesAsFlow(): Flow> +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtworkIndexStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtworkIndexStore.kt new file mode 100644 index 00000000..24c35295 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtworkIndexStore.kt @@ -0,0 +1,17 @@ +package io.github.zyrouge.symphony.services.database.store + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.MapColumn +import androidx.room.OnConflictStrategy +import androidx.room.Query +import io.github.zyrouge.symphony.services.groove.entities.ArtworkIndex + +@Dao +interface ArtworkIndexStore { + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsert(vararg entities: ArtworkIndex): List + + @Query("SELECT * FROM ${ArtworkIndex.TABLE}") + suspend fun entriesSongIdMapped(): Map<@MapColumn(ArtworkIndex.COLUMN_SONG_ID) String, ArtworkIndex> +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtworkCacheStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtworkStore.kt similarity index 53% rename from app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtworkCacheStore.kt rename to app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtworkStore.kt index 8959ce18..80f777c9 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtworkCacheStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtworkStore.kt @@ -4,14 +4,10 @@ import io.github.zyrouge.symphony.Symphony import io.github.zyrouge.symphony.services.database.adapters.FileTreeDatabaseAdapter import java.nio.file.Paths -class ArtworkCacheStore(val symphony: Symphony) { - private val adapter = FileTreeDatabaseAdapter( - Paths - .get(symphony.applicationContext.dataDir.absolutePath, "covers") - .toFile() - ) +class ArtworkStore(val symphony: Symphony) { + private val path = Paths.get(symphony.applicationContext.dataDir.absolutePath, "covers") + private val adapter = FileTreeDatabaseAdapter(path.toFile()) fun get(key: String) = adapter.get(key) fun all() = adapter.list() - fun clear() = adapter.clear() } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreSongMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreSongMappingStore.kt new file mode 100644 index 00000000..d36897e6 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreSongMappingStore.kt @@ -0,0 +1,11 @@ +package io.github.zyrouge.symphony.services.database.store + +import androidx.room.Dao +import androidx.room.Insert +import io.github.zyrouge.symphony.services.groove.entities.GenreSongMapping + +@Dao +interface GenreSongMappingStore { + @Insert + suspend fun insert(vararg entities: GenreSongMapping) +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreStore.kt new file mode 100644 index 00000000..b15a87cf --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreStore.kt @@ -0,0 +1,23 @@ +package io.github.zyrouge.symphony.services.database.store + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Update +import io.github.zyrouge.symphony.services.groove.entities.Genre +import kotlinx.coroutines.flow.Flow + +@Dao +interface GenreStore { + @Insert + suspend fun insert(vararg entities: Genre): List + + @Update + suspend fun update(vararg entities: Genre): Int + + @Query("SELECT * FROM ${Genre.TABLE}") + suspend fun values(): List + + @Query("SELECT * FROM ${Genre.TABLE}") + suspend fun valuesAsFlow(): Flow> +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/LyricsCacheStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/LyricsCacheStore.kt deleted file mode 100644 index 1bf59dd0..00000000 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/LyricsCacheStore.kt +++ /dev/null @@ -1,19 +0,0 @@ -package io.github.zyrouge.symphony.services.database.store - -import io.github.zyrouge.symphony.Symphony -import io.github.zyrouge.symphony.services.database.adapters.SQLiteKeyValueDatabaseAdapter - -class LyricsCacheStore(val symphony: Symphony) { - private val adapter = SQLiteKeyValueDatabaseAdapter( - SQLiteKeyValueDatabaseAdapter.Transformer.AsString(), - SQLiteKeyValueDatabaseAdapter.CacheOpenHelper(symphony.applicationContext, "lyrics", 1) - ) - - fun get(key: String) = adapter.get(key) - fun put(key: String, value: String) = adapter.put(key, value) - fun delete(key: String) = adapter.delete(key) - fun delete(keys: Collection) = adapter.delete(keys) - fun keys() = adapter.keys() - fun all() = adapter.all() - fun clear() = adapter.clear() -} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeFolderStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeFolderStore.kt new file mode 100644 index 00000000..4f9be9a1 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeFolderStore.kt @@ -0,0 +1,29 @@ +package io.github.zyrouge.symphony.services.database.store + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Update +import io.github.zyrouge.symphony.services.groove.entities.MediaTreeFolder +import kotlinx.coroutines.flow.Flow + +@Dao +interface MediaTreeFolderStore { + @Insert() + suspend fun insert(vararg entities: MediaTreeFolder): List + + @Update() + suspend fun update(vararg entities: MediaTreeFolder): Int + + @Query("SELECT * FROM ${MediaTreeFolder.TABLE} WHERE ${MediaTreeFolder.COLUMN_PARENT_ID} = :parentId AND ${MediaTreeFolder.COLUMN_NAME} = :name LIMIT 1") + fun findByName(parentId: String?, name: String): MediaTreeFolder? + + @Query("SELECT * FROM ${MediaTreeFolder.TABLE} WHERE ${MediaTreeFolder.COLUMN_INTERNAL_NAME} = :internalName LIMIT 1") + fun findByInternalName(internalName: String): MediaTreeFolder? + + @Query("SELECT * FROM ${MediaTreeFolder.TABLE}") + fun values(): List + + @Query("SELECT * FROM ${MediaTreeFolder.TABLE}") + fun valuesAsFlow(): Flow> +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeLyricsFileStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeLyricsFileStore.kt new file mode 100644 index 00000000..ea55db31 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeLyricsFileStore.kt @@ -0,0 +1,26 @@ +package io.github.zyrouge.symphony.services.database.store + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Update +import io.github.zyrouge.symphony.services.groove.entities.MediaTreeLyricsFile +import kotlinx.coroutines.flow.Flow + +@Dao +interface MediaTreeLyricsFileStore { + @Insert() + fun insert(vararg entities: MediaTreeLyricsFile): List + + @Update() + fun update(vararg entities: MediaTreeLyricsFile): Int + + @Query("SELECT * FROM ${MediaTreeLyricsFile.TABLE} WHERE ${MediaTreeLyricsFile.COLUMN_PARENT_ID} = :parentId AND ${MediaTreeLyricsFile.COLUMN_NAME} = :name LIMIT 1") + fun findByName(parentId: String, name: String): MediaTreeLyricsFile? + + @Query("SELECT * FROM ${MediaTreeLyricsFile.TABLE}") + suspend fun values(): List + + @Query("SELECT * FROM ${MediaTreeLyricsFile.TABLE}") + suspend fun valuesAsFlow(): Flow> +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeSongFileStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeSongFileStore.kt new file mode 100644 index 00000000..26fd3404 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeSongFileStore.kt @@ -0,0 +1,27 @@ +package io.github.zyrouge.symphony.services.database.store + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Update +import io.github.zyrouge.symphony.services.groove.entities.MediaTreeFolder +import io.github.zyrouge.symphony.services.groove.entities.MediaTreeSongFile +import kotlinx.coroutines.flow.Flow + +@Dao +interface MediaTreeSongFileStore { + @Insert + fun insert(vararg entities: MediaTreeSongFile): List + + @Update + fun update(vararg entities: MediaTreeSongFile): Int + + @Query("SELECT * FROM ${MediaTreeSongFile.TABLE} WHERE ${MediaTreeSongFile.COLUMN_PARENT_ID} = :parentId AND ${MediaTreeSongFile.COLUMN_NAME} = :name LIMIT 1") + fun findByName(parentId: String, name: String): MediaTreeFolder? + + @Query("SELECT * FROM ${MediaTreeSongFile.TABLE}") + suspend fun values(): List + + @Query("SELECT * FROM ${MediaTreeSongFile.TABLE}") + suspend fun valuesAsFlow(): Flow> +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistSongMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistSongMappingStore.kt new file mode 100644 index 00000000..78a15429 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistSongMappingStore.kt @@ -0,0 +1,16 @@ +package io.github.zyrouge.symphony.services.database.store + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Transaction +import io.github.zyrouge.symphony.services.groove.entities.PlaylistSongMapping + +@Dao +interface PlaylistSongMappingStore { + @Insert + suspend fun insert(vararg entities: PlaylistSongMapping) + + @Insert + @Transaction + suspend fun updateInTransaction(vararg entities: PlaylistSongMapping): Int +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistStore.kt index 6ca761e0..524752ca 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistStore.kt @@ -2,22 +2,25 @@ package io.github.zyrouge.symphony.services.database.store import androidx.room.Dao import androidx.room.Insert -import androidx.room.MapColumn import androidx.room.Query import androidx.room.Update -import io.github.zyrouge.symphony.services.groove.Playlist +import io.github.zyrouge.symphony.services.groove.entities.Playlist +import kotlinx.coroutines.flow.Flow @Dao interface PlaylistStore { @Insert - suspend fun insert(vararg playlist: Playlist): List + suspend fun insert(vararg entities: Playlist): List @Update - suspend fun update(vararg playlist: Playlist): Int + suspend fun update(vararg entities: Playlist): Int - @Query("DELETE FROM playlists WHERE id = :playlistId") - suspend fun delete(playlistId: String): Int + @Query("DELETE FROM ${Playlist.TABLE} WHERE ${Playlist.COLUMN_ID} = :id") + suspend fun delete(id: String): Int - @Query("SELECT * FROM playlists") - suspend fun entries(): Map<@MapColumn("id") String, Playlist> + @Query("SELECT * FROM ${Playlist.TABLE}") + suspend fun values(): List + + @Query("SELECT * FROM ${Playlist.TABLE}") + suspend fun valuesAsFlow(): Flow> } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongCacheStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongCacheStore.kt deleted file mode 100644 index cc6d0f5c..00000000 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongCacheStore.kt +++ /dev/null @@ -1,29 +0,0 @@ -package io.github.zyrouge.symphony.services.database.store - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.MapColumn -import androidx.room.Query -import androidx.room.Update -import io.github.zyrouge.symphony.services.groove.Song - -@Dao -interface SongCacheStore { - @Insert() - suspend fun insert(vararg song: Song): List - - @Update - suspend fun update(vararg song: Song): Int - - @Query("DELETE FROM songs WHERE id = :songId") - suspend fun delete(songId: String): Int - - @Query("DELETE FROM songs WHERE id IN (:songIds)") - suspend fun delete(songIds: Collection): Int - - @Query("DELETE FROM songs") - suspend fun clear(): Int - - @Query("SELECT * FROM songs") - suspend fun entriesPathMapped(): Map<@MapColumn("path") String, Song> -} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongFileStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongFileStore.kt new file mode 100644 index 00000000..3b732339 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongFileStore.kt @@ -0,0 +1,29 @@ +package io.github.zyrouge.symphony.services.database.store + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.MapColumn +import androidx.room.Query +import androidx.room.Update +import io.github.zyrouge.symphony.services.groove.entities.SongFile + +@Dao +interface SongFileStore { + @Insert() + suspend fun insert(vararg entities: SongFile): List + + @Update + suspend fun update(vararg entities: SongFile): Int + + @Query("UPDATE ${SongFile.TABLE} SET ${SongFile.COLUMN_DATE_MODIFIED} = 0") + suspend fun updateDateModifiedToZero(): Int + + @Query("DELETE FROM ${SongFile.TABLE} WHERE ${SongFile.COLUMN_ID} = :id") + suspend fun delete(id: String): Int + + @Query("DELETE FROM ${SongFile.TABLE} WHERE ${SongFile.COLUMN_ID} IN (:ids)") + suspend fun delete(ids: Collection): Int + + @Query("SELECT * FROM ${SongFile.TABLE}") + suspend fun entriesPathMapped(): Map<@MapColumn(SongFile.COLUMN_PATH) String, SongFile> +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongLyricsStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongLyricsStore.kt new file mode 100644 index 00000000..297cdb68 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongLyricsStore.kt @@ -0,0 +1,12 @@ +package io.github.zyrouge.symphony.services.database.store + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import io.github.zyrouge.symphony.services.groove.entities.SongLyrics + +@Dao +interface SongLyricsStore { + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsert(vararg entities: SongLyrics) +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongStore.kt new file mode 100644 index 00000000..4428f01c --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongStore.kt @@ -0,0 +1,29 @@ +package io.github.zyrouge.symphony.services.database.store + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Update +import io.github.zyrouge.symphony.services.groove.entities.Song +import kotlinx.coroutines.flow.Flow + +@Dao +interface SongStore { + @Insert() + suspend fun insert(vararg entities: Song): List + + @Update + suspend fun update(vararg entities: Song): Int + + @Query("DELETE FROM ${Song.TABLE} WHERE ${Song.COLUMN_ID} = :id") + suspend fun delete(id: String): Int + + @Query("DELETE FROM ${Song.TABLE} WHERE ${Song.COLUMN_ID} IN (:ids)") + suspend fun delete(ids: Collection): Int + + @Query("SELECT * FROM ${Song.TABLE}") + suspend fun values(): List + + @Query("SELECT * FROM ${Song.TABLE}") + suspend fun valuesAsFlow(): Flow> +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/Album.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/Album.kt deleted file mode 100644 index d9cfc052..00000000 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/Album.kt +++ /dev/null @@ -1,26 +0,0 @@ -package io.github.zyrouge.symphony.services.groove - -import androidx.compose.runtime.Immutable -import io.github.zyrouge.symphony.Symphony -import kotlin.time.Duration - -@Immutable -data class Album( - val id: String, - val name: String, - val artists: MutableSet, - var startYear: Int?, - var endYear: Int?, - var numberOfTracks: Int, - var duration: Duration, -) { - fun createArtworkImageRequest(symphony: Symphony) = - symphony.groove.album.createArtworkImageRequest(id) - - fun getSongIds(symphony: Symphony) = symphony.groove.album.getSongIds(id) - fun getSortedSongIds(symphony: Symphony) = symphony.groove.song.sort( - getSongIds(symphony), - symphony.settings.lastUsedAlbumSongsSortBy.value, - symphony.settings.lastUsedAlbumSongsSortReverse.value, - ) -} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/AlbumArtist.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/AlbumArtist.kt deleted file mode 100644 index 796cf3b3..00000000 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/AlbumArtist.kt +++ /dev/null @@ -1,23 +0,0 @@ -package io.github.zyrouge.symphony.services.groove - -import androidx.compose.runtime.Immutable -import io.github.zyrouge.symphony.Symphony - -@Immutable -data class AlbumArtist( - val name: String, - var numberOfAlbums: Int, - var numberOfTracks: Int, -) { - fun createArtworkImageRequest(symphony: Symphony) = - symphony.groove.albumArtist.createArtworkImageRequest(name) - - fun getSongIds(symphony: Symphony) = symphony.groove.albumArtist.getSongIds(name) - fun getSortedSongIds(symphony: Symphony) = symphony.groove.song.sort( - getSongIds(symphony), - symphony.settings.lastUsedSongsSortBy.value, - symphony.settings.lastUsedSongsSortReverse.value, - ) - - fun getAlbumIds(symphony: Symphony) = symphony.groove.albumArtist.getAlbumIds(name) -} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/Genre.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/Genre.kt deleted file mode 100644 index 36e03810..00000000 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/Genre.kt +++ /dev/null @@ -1,17 +0,0 @@ -package io.github.zyrouge.symphony.services.groove - -import androidx.compose.runtime.Immutable -import io.github.zyrouge.symphony.Symphony - -@Immutable -data class Genre( - val name: String, - var numberOfTracks: Int, -) { - fun getSongIds(symphony: Symphony) = symphony.groove.genre.getSongIds(name) - fun getSortedSongIds(symphony: Symphony) = symphony.groove.song.sort( - getSongIds(symphony), - symphony.settings.lastUsedSongsSortBy.value, - symphony.settings.lastUsedSongsSortReverse.value, - ) -} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/MediaExposer.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/MediaExposer.kt index 4a7ead5c..8a64a4dc 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/MediaExposer.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/MediaExposer.kt @@ -1,12 +1,19 @@ package io.github.zyrouge.symphony.services.groove -import android.net.Uri +import android.graphics.Bitmap +import android.graphics.BitmapFactory import io.github.zyrouge.symphony.Symphony +import io.github.zyrouge.symphony.services.groove.entities.ArtworkIndex +import io.github.zyrouge.symphony.services.groove.entities.MediaTreeFolder +import io.github.zyrouge.symphony.services.groove.entities.MediaTreeLyricsFile +import io.github.zyrouge.symphony.services.groove.entities.MediaTreeSongFile +import io.github.zyrouge.symphony.services.groove.entities.SongFile +import io.github.zyrouge.symphony.services.groove.entities.SongLyrics import io.github.zyrouge.symphony.utils.ActivityUtils import io.github.zyrouge.symphony.utils.ConcurrentSet import io.github.zyrouge.symphony.utils.DocumentFileX +import io.github.zyrouge.symphony.utils.ImagePreserver import io.github.zyrouge.symphony.utils.Logger -import io.github.zyrouge.symphony.utils.SimpleFileSystem import io.github.zyrouge.symphony.utils.SimplePath import io.github.zyrouge.symphony.utils.concurrentSetOf import kotlinx.coroutines.Dispatchers @@ -17,51 +24,16 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update -import kotlinx.coroutines.withContext +import java.io.FileOutputStream import java.util.concurrent.ConcurrentHashMap import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds class MediaExposer(private val symphony: Symphony) { - internal val uris = ConcurrentHashMap() - var explorer = SimpleFileSystem.Folder() private val _isUpdating = MutableStateFlow(false) val isUpdating = _isUpdating.asStateFlow() - private fun emitUpdate(value: Boolean) = _isUpdating.update { - value - } - - private data class ScanCycle( - val songCache: ConcurrentHashMap, - val songCacheUnused: ConcurrentSet, - val artworkCacheUnused: ConcurrentSet, - val lyricsCacheUnused: ConcurrentSet, - val filter: MediaFilter, - val songParseOptions: Song.ParseOptions, - ) { - companion object { - suspend fun create(symphony: Symphony): ScanCycle { - val songCache = ConcurrentHashMap(symphony.database.songCache.entriesPathMapped()) - val songCacheUnused = concurrentSetOf(songCache.map { it.value.id }) - val artworkCacheUnused = concurrentSetOf(symphony.database.artworkCache.all()) - val lyricsCacheUnused = concurrentSetOf(symphony.database.lyricsCache.keys()) - val filter = MediaFilter( - symphony.settings.songsFilterPattern.value, - symphony.settings.blacklistFolders.value.toSortedSet(), - symphony.settings.whitelistFolders.value.toSortedSet() - ) - return ScanCycle( - songCache = songCache, - songCacheUnused = songCacheUnused, - artworkCacheUnused = artworkCacheUnused, - lyricsCacheUnused = lyricsCacheUnused, - filter = filter, - songParseOptions = Song.ParseOptions.create(symphony), - ) - } - } - } + private fun emitUpdate(value: Boolean) = _isUpdating.update { value } @OptIn(ExperimentalCoroutinesApi::class) suspend fun fetch() { @@ -69,17 +41,17 @@ class MediaExposer(private val symphony: Symphony) { try { val context = symphony.applicationContext val folderUris = symphony.settings.mediaFolders.value - val cycle = ScanCycle.create(symphony) + val scanner = Scanner.create(symphony) folderUris.map { x -> ActivityUtils.makePersistableReadableUri(context, x) DocumentFileX.fromTreeUri(context, x)?.let { val path = SimplePath(DocumentFileX.getParentPathOfTreeUri(x) ?: it.name) with(Dispatchers.IO) { - scanMediaTree(cycle, path, it) + scanner.scanMediaTree(path, scanner.root, it) } } } - trimCache(cycle) + scanner.cleanup() } catch (err: Exception) { Logger.error("MediaExposer", "fetch failed", err) } @@ -87,125 +59,232 @@ class MediaExposer(private val symphony: Symphony) { emitFinish() } - private suspend fun scanMediaTree(cycle: ScanCycle, path: SimplePath, file: DocumentFileX) { - try { - if (!cycle.filter.isWhitelisted(path.pathString)) { - return - } - coroutineScope { - file.list().map { - val childPath = path.join(it.name) - async { - when { - it.isDirectory -> scanMediaTree(cycle, childPath, it) - else -> scanMediaFile(cycle, childPath, it) + private data class Scanner( + val symphony: Symphony, + val songCache: ConcurrentHashMap, + val songStaleIds: ConcurrentSet, + val artworkIndexCache: ConcurrentHashMap, + val artworkStaleFiles: ConcurrentSet, + val root: MediaTreeFolder, + val filter: MediaFilter, + val songParseOptions: SongFile.ParseOptions, + ) { + suspend fun scanMediaTree(path: SimplePath, parent: MediaTreeFolder, xfile: DocumentFileX) { + try { + if (!filter.isWhitelisted(path.pathString)) { + return + } + val exParent = symphony.database.mediaTreeFolders.findByName(parent.id, xfile.name) + if (exParent?.dateModified == xfile.lastModified) { + return + } + val nParent = exParent ?: MediaTreeFolder( + id = symphony.database.mediaTreeFoldersIdGenerator.next(), + parentId = parent.id, + internalName = null, + name = xfile.name, + uri = xfile.uri, + dateModified = 0, // change it after scanning is done + ) + if (exParent == null) { + symphony.database.mediaTreeFolders.insert(nParent) + } + coroutineScope { + xfile.list().map { + val nPath = path.join(it.name) + async { + when { + it.isDirectory -> scanMediaTree(nPath, nParent, it) + else -> scanMediaFile(nPath, nParent, it) + } } - } - }.awaitAll() + }.awaitAll() + } + symphony.database.mediaTreeFolders.update( + nParent.copy(dateModified = xfile.lastModified), + ) + } catch (err: Exception) { + Logger.error("MediaExposer", "scan media tree failed", err) } - } catch (err: Exception) { - Logger.error("MediaExposer", "scan media tree failed", err) } - } - private suspend fun scanMediaFile(cycle: ScanCycle, path: SimplePath, file: DocumentFileX) { - try { - when { - path.extension == "lrc" -> scanLrcFile(cycle, path, file) - file.mimeType == MIMETYPE_M3U -> scanM3UFile(cycle, path, file) - file.mimeType.startsWith("audio/") -> scanAudioFile(cycle, path, file) + suspend fun scanMediaFile(path: SimplePath, parent: MediaTreeFolder, xfile: DocumentFileX) { + try { + when { + path.extension == "lrc" -> scanLrcFile(path, parent, xfile) + xfile.mimeType.startsWith("audio/") -> scanAudioFile(path, parent, xfile) + } + } catch (err: Exception) { + Logger.error("MediaExposer", "scan media file failed", err) } - } catch (err: Exception) { - Logger.error("MediaExposer", "scan media file failed", err) } - } - private suspend fun scanAudioFile(cycle: ScanCycle, path: SimplePath, file: DocumentFileX) { - val pathString = path.pathString - uris[pathString] = file.uri - val lastModified = file.lastModified - val cached = cycle.songCache[pathString] - val cacheHit = cached != null - && cached.dateModified == lastModified - && (cached.coverFile?.let { cycle.artworkCacheUnused.contains(it) } != false) - val song = when { - cacheHit -> cached - else -> Song.parse(path, file, cycle.songParseOptions) - } - if (song.duration.milliseconds < symphony.settings.minSongDuration.value.seconds) { - return - } - if (!cacheHit) { - symphony.database.songCache.insert(song) - cached?.coverFile?.let { - if (symphony.database.artworkCache.get(it).delete()) { - cycle.artworkCacheUnused.remove(it) + suspend fun scanAudioFile(path: SimplePath, parent: MediaTreeFolder, xfile: DocumentFileX) { + val exSongFile = songCache[path.pathString] + val exArtworkIndex = artworkIndexCache[exSongFile?.id] + val skipArtworkParsing = exArtworkIndex != null && exArtworkIndex.let { + exArtworkIndex.file == null || artworkStaleFiles.contains(exArtworkIndex.file) + } + val skipParsing = skipArtworkParsing && exSongFile?.dateModified == xfile.lastModified + val state: SongFileState + val songFile: SongFile + val artworkIndex: ArtworkIndex + when { + skipParsing -> { + state = SongFileState.Existing + songFile = exSongFile + artworkIndex = exArtworkIndex + } + + else -> { + state = when { + exSongFile != null -> SongFileState.Updated + else -> SongFileState.New + } + val id = exSongFile?.id ?: symphony.database.songsIdGenerator.next() + val extended = SongFile.parse(id, path, xfile, songParseOptions) + songFile = extended.songFile + val artworkFile = extended.artwork?.let { + val extension = when (it.mimeType) { + "image/jpg", "image/jpeg" -> "jpg" + "image/png" -> "png" + "_" -> "_" + else -> null + } + if (extension == null) { + return@let null + } + val quality = symphony.settings.artworkQuality.value + if (quality.maxSide == null && extension != "_") { + val name = "$id.$extension" + symphony.database.artwork.get(name).writeBytes(it.data) + return@let name + } + val bitmap = BitmapFactory.decodeByteArray(it.data, 0, it.data.size) + val name = "$id.jpg" + FileOutputStream(symphony.database.artwork.get(name)).use { writer -> + ImagePreserver + .resize(bitmap, quality) + .compress(Bitmap.CompressFormat.JPEG, 100, writer) + } + name + } + artworkIndex = ArtworkIndex(songId = id, file = artworkFile) + extended.lyrics?.let { + symphony.database.songLyrics.upsert(SongLyrics(id, it)) + } + symphony.database.songFiles.update(songFile) } } + if (!skipArtworkParsing || !skipParsing) { + symphony.database.artworkIndices.upsert(artworkIndex) + } + artworkIndex.file?.let { + artworkStaleFiles.remove(it) + } + songStaleIds.remove(songFile.id) + val exFile = symphony.database.mediaTreeSongFiles.findByName(parent.id, xfile.name) + val file = MediaTreeSongFile( + id = exFile?.id ?: symphony.database.mediaTreeSongFilesIdGenerator.next(), + parentId = parent.id, + songFileId = songFile.id, + name = xfile.name, + uri = xfile.uri, + dateModified = xfile.lastModified, + ) + when { + exFile == null -> symphony.database.mediaTreeSongFiles.insert(file) + else -> symphony.database.mediaTreeSongFiles.update(file) + } + if (songFile.duration.milliseconds < symphony.settings.minSongDuration.value.seconds) { + return + } + symphony.groove.exposer.emitSongFile(state, songFile) } - cycle.songCacheUnused.remove(song.id) - song.coverFile?.let { - cycle.artworkCacheUnused.remove(it) - } - cycle.lyricsCacheUnused.remove(song.id) - explorer.addChildFile(path) - withContext(Dispatchers.Main) { - emitSong(song) - } - } - - private fun scanLrcFile( - @Suppress("Unused") cycle: ScanCycle, - path: SimplePath, - file: DocumentFileX, - ) { - uris[path.pathString] = file.uri - explorer.addChildFile(path) - } - private fun scanM3UFile( - @Suppress("Unused") cycle: ScanCycle, - path: SimplePath, - file: DocumentFileX, - ) { - uris[path.pathString] = file.uri - explorer.addChildFile(path) - } - - private suspend fun trimCache(cycle: ScanCycle) { - try { - symphony.database.songCache.delete(cycle.songCacheUnused) - } catch (err: Exception) { - Logger.warn("MediaExposer", "trim song cache failed", err) + fun scanLrcFile( + @Suppress("Unused") path: SimplePath, + parent: MediaTreeFolder, + xfile: DocumentFileX, + ) { + val exFile = symphony.database.mediaTreeLyricsFiles.findByName(parent.id, xfile.name) + if (exFile?.dateModified == xfile.lastModified) { + return + } + val file = MediaTreeLyricsFile( + id = exFile?.id ?: symphony.database.mediaTreeLyricsFilesIdGenerator.next(), + parentId = parent.id, + name = xfile.name, + uri = xfile.uri, + dateModified = xfile.lastModified, + ) + when { + exFile == null -> symphony.database.mediaTreeLyricsFiles.insert(file) + else -> symphony.database.mediaTreeLyricsFiles.update(file) + } } - for (x in cycle.artworkCacheUnused) { + + suspend fun cleanup() { try { - symphony.database.artworkCache.get(x).delete() + symphony.database.songFiles.delete(songStaleIds) } catch (err: Exception) { - Logger.warn("MediaExposer", "delete artwork cache file failed", err) + Logger.warn("MediaExposer", "trimming song files failed", err) + } + for (x in artworkStaleFiles) { + try { + symphony.database.artwork.get(x).delete() + } catch (err: Exception) { + Logger.warn("MediaExposer", "deleting artwork failed", err) + } } } - try { - symphony.database.lyricsCache.delete(cycle.lyricsCacheUnused) - } catch (err: Exception) { - Logger.warn("MediaExposer", "trim lyrics cache failed", err) + + companion object { + suspend fun create(symphony: Symphony): Scanner { + val filter = MediaFilter( + symphony.settings.songsFilterPattern.value, + symphony.settings.blacklistFolders.value.toSortedSet(), + symphony.settings.whitelistFolders.value.toSortedSet() + ) + val songEntries = symphony.database.songFiles.entriesPathMapped() + return Scanner( + symphony = symphony, + songCache = ConcurrentHashMap(songEntries), + songStaleIds = concurrentSetOf(songEntries.keys), + artworkIndexCache = ConcurrentHashMap(symphony.database.artworkIndices.entriesSongIdMapped()), + artworkStaleFiles = concurrentSetOf(symphony.database.artwork.all()), + root = getTreeRootFolder(symphony), + filter = filter, + songParseOptions = SongFile.ParseOptions.create(symphony), + ) + } + + private suspend fun getTreeRootFolder(symphony: Symphony): MediaTreeFolder { + symphony.database.mediaTreeFolders.findByInternalName(MEDIA_TREE_ROOT_NAME)?.let { + return it + } + val folder = MediaTreeFolder( + id = symphony.database.mediaTreeFoldersIdGenerator.next(), + parentId = null, + internalName = MEDIA_TREE_ROOT_NAME, + name = MEDIA_TREE_ROOT_NAME, + uri = null, + dateModified = 0, + ) + symphony.database.mediaTreeFolders.insert(folder) + return folder + } } } - suspend fun reset() { - emitUpdate(true) - uris.clear() - explorer = SimpleFileSystem.Folder() - symphony.database.songCache.clear() - emitUpdate(false) + enum class SongFileState { + New, + Updated, + Existing, } - private fun emitSong(song: Song) { - symphony.groove.albumArtist.onSong(song) - symphony.groove.album.onSong(song) - symphony.groove.artist.onSong(song) - symphony.groove.genre.onSong(song) - symphony.groove.song.onSong(song) + private fun emitSongFile(state: SongFileState, songFile: SongFile) { + symphony.groove.song.onSongFile(state, songFile) } private fun emitFinish() { @@ -239,6 +318,6 @@ class MediaExposer(private val symphony: Symphony) { } companion object { - const val MIMETYPE_M3U = "audio/x-mpegurl" + const val MEDIA_TREE_ROOT_NAME = "root" } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Album.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Album.kt new file mode 100644 index 00000000..def3579d --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Album.kt @@ -0,0 +1,43 @@ +package io.github.zyrouge.symphony.services.groove.entities + +import androidx.compose.runtime.Immutable +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey +import io.github.zyrouge.symphony.Symphony + +@Immutable +@Entity( + Album.TABLE, + indices = [Index(Album.COLUMN_NAME)], +) +data class Album( + @PrimaryKey + @ColumnInfo(COLUMN_ID) + val id: String, + @ColumnInfo(COLUMN_NAME) + val name: String, + @ColumnInfo(COLUMN_START_YEAR) + val startYear: Int?, + @ColumnInfo(COLUMN_END_YEAR) + val endYear: Int?, +) { + fun createArtworkImageRequest(symphony: Symphony) = + symphony.groove.album.createArtworkImageRequest(id) + + fun getSongIds(symphony: Symphony) = symphony.groove.album.getSongIds(id) + fun getSortedSongIds(symphony: Symphony) = symphony.groove.song.sort( + getSongIds(symphony), + symphony.settings.lastUsedAlbumSongsSortBy.value, + symphony.settings.lastUsedAlbumSongsSortReverse.value, + ) + + companion object { + const val TABLE = "albums" + const val COLUMN_ID = "id" + const val COLUMN_NAME = "name" + const val COLUMN_START_YEAR = "start_year" + const val COLUMN_END_YEAR = "end_year" + } +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/AlbumArtistMapping.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/AlbumArtistMapping.kt new file mode 100644 index 00000000..802286d3 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/AlbumArtistMapping.kt @@ -0,0 +1,46 @@ +package io.github.zyrouge.symphony.services.groove.entities + +import androidx.compose.runtime.Immutable +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index + +@Immutable +@Entity( + AlbumArtistMapping.TABLE, + foreignKeys = [ + ForeignKey( + entity = Album::class, + parentColumns = arrayOf(Album.COLUMN_ID), + childColumns = arrayOf(AlbumArtistMapping.COLUMN_ARTIST_ID), + onDelete = ForeignKey.CASCADE, + ), + ForeignKey( + entity = Artist::class, + parentColumns = arrayOf(Song.COLUMN_ID), + childColumns = arrayOf(AlbumArtistMapping.COLUMN_ARTIST_ID), + onDelete = ForeignKey.CASCADE, + ), + ], + indices = [ + Index(AlbumArtistMapping.COLUMN_ALBUM_ID), + Index(AlbumArtistMapping.COLUMN_ARTIST_ID), + Index(AlbumArtistMapping.COLUMN_IS_ALBUM_ARTIST), + ], +) +data class AlbumArtistMapping( + @ColumnInfo(COLUMN_ALBUM_ID) + val albumId: String, + @ColumnInfo(COLUMN_ARTIST_ID) + val artistId: String, + @ColumnInfo(COLUMN_IS_ALBUM_ARTIST) + val isAlbumArtist: Boolean, +) { + companion object { + const val TABLE = "album_artists_mapping" + const val COLUMN_ALBUM_ID = "album_id" + const val COLUMN_ARTIST_ID = "artist_id" + const val COLUMN_IS_ALBUM_ARTIST = "is_album_artist" + } +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/AlbumSongMapping.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/AlbumSongMapping.kt new file mode 100644 index 00000000..19955e41 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/AlbumSongMapping.kt @@ -0,0 +1,39 @@ +package io.github.zyrouge.symphony.services.groove.entities + +import androidx.compose.runtime.Immutable +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index + +@Immutable +@Entity( + AlbumSongMapping.TABLE, + foreignKeys = [ + ForeignKey( + entity = Album::class, + parentColumns = arrayOf(Album.COLUMN_ID), + childColumns = arrayOf(AlbumSongMapping.COLUMN_ALBUM_ID), + onDelete = ForeignKey.CASCADE, + ), + ForeignKey( + entity = Song::class, + parentColumns = arrayOf(Song.COLUMN_ID), + childColumns = arrayOf(AlbumSongMapping.COLUMN_SONG_ID), + onDelete = ForeignKey.CASCADE, + ), + ], + indices = [Index(AlbumSongMapping.COLUMN_ALBUM_ID)], +) +data class AlbumSongMapping( + @ColumnInfo(COLUMN_ALBUM_ID) + val albumId: String, + @ColumnInfo(COLUMN_SONG_ID) + val songId: String, +) { + companion object { + const val TABLE = "album_songs_mapping" + const val COLUMN_ALBUM_ID = "album_id" + const val COLUMN_SONG_ID = "song_id" + } +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/Artist.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Artist.kt similarity index 57% rename from app/src/main/java/io/github/zyrouge/symphony/services/groove/Artist.kt rename to app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Artist.kt index 0654407c..fc878638 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/Artist.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Artist.kt @@ -1,13 +1,23 @@ -package io.github.zyrouge.symphony.services.groove +package io.github.zyrouge.symphony.services.groove.entities import androidx.compose.runtime.Immutable +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey import io.github.zyrouge.symphony.Symphony @Immutable +@Entity( + Artist.TABLE, + indices = [Index(Artist.COLUMN_NAME)], +) data class Artist( + @PrimaryKey + @ColumnInfo(COLUMN_ID) + val id: String, + @ColumnInfo(COLUMN_NAME) val name: String, - var numberOfAlbums: Int, - var numberOfTracks: Int, ) { fun createArtworkImageRequest(symphony: Symphony) = symphony.groove.artist.createArtworkImageRequest(name) @@ -20,4 +30,10 @@ data class Artist( ) fun getAlbumIds(symphony: Symphony) = symphony.groove.artist.getAlbumIds(name) + + companion object { + const val TABLE = "artists" + const val COLUMN_ID = "id" + const val COLUMN_NAME = "name" + } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/ArtistSongMapping.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/ArtistSongMapping.kt new file mode 100644 index 00000000..dd15a6eb --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/ArtistSongMapping.kt @@ -0,0 +1,39 @@ +package io.github.zyrouge.symphony.services.groove.entities + +import androidx.compose.runtime.Immutable +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index + +@Immutable +@Entity( + ArtistSongMapping.TABLE, + foreignKeys = [ + ForeignKey( + entity = AlbumArtistMapping::class, + parentColumns = arrayOf(Artist.COLUMN_ID), + childColumns = arrayOf(ArtistSongMapping.COLUMN_ARTIST_ID), + onDelete = ForeignKey.CASCADE, + ), + ForeignKey( + entity = Song::class, + parentColumns = arrayOf(Song.COLUMN_ID), + childColumns = arrayOf(ArtistSongMapping.COLUMN_SONG_ID), + onDelete = ForeignKey.CASCADE, + ), + ], + indices = [Index(ArtistSongMapping.COLUMN_ARTIST_ID)], +) +data class ArtistSongMapping( + @ColumnInfo(COLUMN_ARTIST_ID) + val artistId: String, + @ColumnInfo(COLUMN_SONG_ID) + val songId: String, +) { + companion object { + const val TABLE = "artist_songs_mapping" + const val COLUMN_ARTIST_ID = "artist_id" + const val COLUMN_SONG_ID = "song_id" + } +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/ArtworkIndex.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/ArtworkIndex.kt new file mode 100644 index 00000000..a7e63d5e --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/ArtworkIndex.kt @@ -0,0 +1,35 @@ +package io.github.zyrouge.symphony.services.groove.entities + +import androidx.compose.runtime.Immutable +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey + +@Immutable +@Entity( + ArtworkIndex.TABLE, + foreignKeys = [ + ForeignKey( + entity = Song::class, + parentColumns = arrayOf(Song.COLUMN_ID), + childColumns = arrayOf(ArtworkIndex.COLUMN_SONG_ID), + onDelete = ForeignKey.CASCADE, + ), + ], + indices = [Index(ArtworkIndex.COLUMN_FILE)], +) +data class ArtworkIndex( + @PrimaryKey + @ColumnInfo(COLUMN_SONG_ID) + val songId: String, + @ColumnInfo(COLUMN_FILE) + val file: String?, +) { + companion object { + const val TABLE = "artwork_indices" + const val COLUMN_SONG_ID = "song_id" + const val COLUMN_FILE = "file" + } +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Genre.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Genre.kt new file mode 100644 index 00000000..18155ca8 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Genre.kt @@ -0,0 +1,34 @@ +package io.github.zyrouge.symphony.services.groove.entities + +import androidx.compose.runtime.Immutable +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey +import io.github.zyrouge.symphony.Symphony + +@Immutable +@Entity( + Genre.TABLE, + indices = [Index(Genre.COLUMN_NAME)], +) +data class Genre( + @PrimaryKey + @ColumnInfo(COLUMN_ID) + val id: String, + @ColumnInfo(COLUMN_NAME) + val name: String, +) { + fun getSongIds(symphony: Symphony) = symphony.groove.genre.getSongIds(name) + fun getSortedSongIds(symphony: Symphony) = symphony.groove.song.sort( + getSongIds(symphony), + symphony.settings.lastUsedSongsSortBy.value, + symphony.settings.lastUsedSongsSortReverse.value, + ) + + companion object { + const val TABLE = "genres" + const val COLUMN_ID = "id" + const val COLUMN_NAME = "name" + } +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/GenreSongMapping.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/GenreSongMapping.kt new file mode 100644 index 00000000..4458ba36 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/GenreSongMapping.kt @@ -0,0 +1,39 @@ +package io.github.zyrouge.symphony.services.groove.entities + +import androidx.compose.runtime.Immutable +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index + +@Immutable +@Entity( + GenreSongMapping.TABLE, + foreignKeys = [ + ForeignKey( + entity = AlbumArtistMapping::class, + parentColumns = arrayOf(Genre.COLUMN_ID), + childColumns = arrayOf(GenreSongMapping.GENRE_ID), + onDelete = ForeignKey.CASCADE, + ), + ForeignKey( + entity = Song::class, + parentColumns = arrayOf(Song.COLUMN_ID), + childColumns = arrayOf(GenreSongMapping.COLUMN_SONG_ID), + onDelete = ForeignKey.CASCADE, + ), + ], + indices = [Index(GenreSongMapping.GENRE_ID)], +) +data class GenreSongMapping( + @ColumnInfo(GENRE_ID) + val genreId: String, + @ColumnInfo(COLUMN_SONG_ID) + val songId: String, +) { + companion object { + const val TABLE = "artist_songs_mapping" + const val GENRE_ID = "genre_id" + const val COLUMN_SONG_ID = "song_id" + } +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/MediaTreeFolder.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/MediaTreeFolder.kt new file mode 100644 index 00000000..2a2ff735 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/MediaTreeFolder.kt @@ -0,0 +1,52 @@ +package io.github.zyrouge.symphony.services.groove.entities + +import android.net.Uri +import androidx.compose.runtime.Immutable +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey + +@Immutable +@Entity( + MediaTreeFolder.TABLE, + foreignKeys = [ + ForeignKey( + entity = MediaTreeFolder::class, + parentColumns = arrayOf(MediaTreeFolder.COLUMN_ID), + childColumns = arrayOf(MediaTreeFolder.COLUMN_PARENT_ID), + onDelete = ForeignKey.CASCADE, + ), + ], + indices = [ + Index(MediaTreeFolder.COLUMN_PARENT_ID), + Index(MediaTreeFolder.COLUMN_INTERNAL_NAME, unique = true), + Index(MediaTreeFolder.COLUMN_NAME), + ] +) +data class MediaTreeFolder( + @PrimaryKey + @ColumnInfo(COLUMN_ID) + val id: String, + @ColumnInfo(COLUMN_PARENT_ID) + val parentId: String?, + @ColumnInfo(COLUMN_INTERNAL_NAME) + val internalName: String?, + @ColumnInfo(COLUMN_NAME) + val name: String, + @ColumnInfo(COLUMN_URI) + val uri: Uri?, + @ColumnInfo(COLUMN_DATE_MODIFIED) + val dateModified: Long, +) { + companion object { + const val TABLE = "media_tree_folders" + const val COLUMN_ID = "id" + const val COLUMN_PARENT_ID = "parent_id" + const val COLUMN_INTERNAL_NAME = "internal_name" + const val COLUMN_NAME = "name" + const val COLUMN_URI = "uri" + const val COLUMN_DATE_MODIFIED = "date_modified" + } +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/MediaTreeLyricsFile.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/MediaTreeLyricsFile.kt new file mode 100644 index 00000000..1007d1d7 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/MediaTreeLyricsFile.kt @@ -0,0 +1,54 @@ +package io.github.zyrouge.symphony.services.groove.entities + +import android.net.Uri +import androidx.compose.runtime.Immutable +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey + +@Immutable +@Entity( + MediaTreeLyricsFile.TABLE, + foreignKeys = [ + ForeignKey( + entity = MediaTreeFolder::class, + parentColumns = arrayOf(MediaTreeFolder.COLUMN_ID), + childColumns = arrayOf(MediaTreeLyricsFile.COLUMN_PARENT_ID), + onDelete = ForeignKey.CASCADE, + ), + ForeignKey( + entity = Playlist::class, + parentColumns = arrayOf(Song.COLUMN_ID), + childColumns = arrayOf(MediaTreeLyricsFile.COLUMN_NAME), + onDelete = ForeignKey.CASCADE, + ), + ], + indices = [ + Index(MediaTreeLyricsFile.COLUMN_PARENT_ID), + Index(MediaTreeLyricsFile.COLUMN_NAME), + ], +) +data class MediaTreeLyricsFile( + @PrimaryKey + @ColumnInfo(COLUMN_ID) + val id: String, + @ColumnInfo(COLUMN_PARENT_ID) + val parentId: String, + @ColumnInfo(COLUMN_NAME) + val name: String, + @ColumnInfo(COLUMN_URI) + val uri: Uri, + @ColumnInfo(COLUMN_DATE_MODIFIED) + val dateModified: Long, +) { + companion object { + const val TABLE = "media_tree_song_files" + const val COLUMN_ID = "id" + const val COLUMN_PARENT_ID = "parent_id" + const val COLUMN_NAME = "name" + const val COLUMN_URI = "uri" + const val COLUMN_DATE_MODIFIED = "date_modified" + } +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/MediaTreeSongFile.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/MediaTreeSongFile.kt new file mode 100644 index 00000000..c3aacb75 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/MediaTreeSongFile.kt @@ -0,0 +1,57 @@ +package io.github.zyrouge.symphony.services.groove.entities + +import android.net.Uri +import androidx.compose.runtime.Immutable +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey + +@Immutable +@Entity( + MediaTreeSongFile.TABLE, + foreignKeys = [ + ForeignKey( + entity = MediaTreeFolder::class, + parentColumns = arrayOf(MediaTreeFolder.COLUMN_ID), + childColumns = arrayOf(MediaTreeSongFile.COLUMN_PARENT_ID), + onDelete = ForeignKey.CASCADE, + ), + ForeignKey( + entity = SongFile::class, + parentColumns = arrayOf(SongFile.COLUMN_ID), + childColumns = arrayOf(MediaTreeSongFile.COLUMN_SONG_FILE_ID), + onDelete = ForeignKey.CASCADE, + ), + ], + indices = [ + Index(MediaTreeSongFile.COLUMN_PARENT_ID), + Index(MediaTreeSongFile.COLUMN_NAME), + ], +) +data class MediaTreeSongFile( + @PrimaryKey + @ColumnInfo(COLUMN_ID) + val id: String, + @ColumnInfo(COLUMN_PARENT_ID) + val parentId: String, + @ColumnInfo(COLUMN_SONG_FILE_ID) + val songFileId: String, + @ColumnInfo(COLUMN_NAME) + val name: String, + @ColumnInfo(COLUMN_URI) + val uri: Uri, + @ColumnInfo(COLUMN_DATE_MODIFIED) + val dateModified: Long, +) { + companion object { + const val TABLE = "media_tree_song_files" + const val COLUMN_ID = "id" + const val COLUMN_PARENT_ID = "parent_id" + const val COLUMN_SONG_FILE_ID = "song_file_id" + const val COLUMN_NAME = "name" + const val COLUMN_URI = "uri" + const val COLUMN_DATE_MODIFIED = "date_modified" + } +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/Playlist.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Playlist.kt similarity index 85% rename from app/src/main/java/io/github/zyrouge/symphony/services/groove/Playlist.kt rename to app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Playlist.kt index 4810fc9c..4e05595a 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/Playlist.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Playlist.kt @@ -1,8 +1,10 @@ -package io.github.zyrouge.symphony.services.groove +package io.github.zyrouge.symphony.services.groove.entities import android.net.Uri import androidx.compose.runtime.Immutable +import androidx.room.ColumnInfo import androidx.room.Entity +import androidx.room.Index import androidx.room.PrimaryKey import io.github.zyrouge.symphony.Symphony import io.github.zyrouge.symphony.ui.helpers.Assets @@ -12,13 +14,20 @@ import kotlin.io.path.Path import kotlin.io.path.nameWithoutExtension @Immutable -@Entity("playlists") +@Entity( + Playlist.TABLE, + indices = [Index(Playlist.COLUMN_TITLE)], +) data class Playlist( @PrimaryKey + @ColumnInfo(COLUMN_ID) val id: String, + @ColumnInfo(COLUMN_TITLE) val title: String, val songPaths: List, + @ColumnInfo(COLUMN_URI) val uri: Uri?, + @ColumnInfo(COLUMN_PATH) val path: String?, ) { val numberOfTracks: Int get() = songPaths.size @@ -58,6 +67,12 @@ data class Playlist( ) companion object { + const val TABLE = "playlists" + const val COLUMN_ID = "id" + const val COLUMN_TITLE = "title" + const val COLUMN_URI = "title" + const val COLUMN_PATH = "path" + private const val PRIMARY_STORAGE = "primary:" fun parse(symphony: Symphony, playlistId: String?, uri: Uri): Playlist { diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/PlaylistSongMapping.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/PlaylistSongMapping.kt new file mode 100644 index 00000000..5854feea --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/PlaylistSongMapping.kt @@ -0,0 +1,60 @@ +package io.github.zyrouge.symphony.services.groove.entities + +import androidx.compose.runtime.Immutable +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey + +@Immutable +@Entity( + PlaylistSongMapping.TABLE, + foreignKeys = [ + ForeignKey( + entity = Playlist::class, + parentColumns = arrayOf(Playlist.COLUMN_ID), + childColumns = arrayOf(PlaylistSongMapping.COLUMN_PLAYLIST_ID), + onDelete = ForeignKey.CASCADE, + ), + ForeignKey( + entity = Song::class, + parentColumns = arrayOf(Song.COLUMN_ID), + childColumns = arrayOf(PlaylistSongMapping.COLUMN_SONG_ID), + onDelete = ForeignKey.SET_NULL, + ), + ForeignKey( + entity = PlaylistSongMapping::class, + parentColumns = arrayOf(PlaylistSongMapping.COLUMN_ID), + childColumns = arrayOf(PlaylistSongMapping.COLUMN_NEXT_ID), + onDelete = ForeignKey.SET_NULL, + ), + ], + indices = [ + Index(PlaylistSongMapping.COLUMN_PLAYLIST_ID), + Index(PlaylistSongMapping.COLUMN_IS_HEAD), + Index(PlaylistSongMapping.COLUMN_NEXT_ID), + ], +) +data class PlaylistSongMapping( + @PrimaryKey + @ColumnInfo(COLUMN_ID) + val id: String, + @ColumnInfo(COLUMN_PLAYLIST_ID) + val playlistId: String, + @ColumnInfo(COLUMN_SONG_ID) + val songId: String?, + @ColumnInfo(COLUMN_IS_HEAD) + val isStart: Boolean, + @ColumnInfo(COLUMN_NEXT_ID) + val nextId: String?, +) { + companion object { + const val TABLE = "playlist_songs" + const val COLUMN_ID = "id" + const val COLUMN_PLAYLIST_ID = "playlist_id" + const val COLUMN_SONG_ID = "song_id" + const val COLUMN_IS_HEAD = "is_head" + const val COLUMN_NEXT_ID = "next_id" + } +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Song.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Song.kt new file mode 100644 index 00000000..7104a03f --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Song.kt @@ -0,0 +1,127 @@ +package io.github.zyrouge.symphony.services.groove.entities + +import android.net.Uri +import androidx.compose.runtime.Immutable +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey +import io.github.zyrouge.symphony.Symphony +import io.github.zyrouge.symphony.utils.SimplePath +import java.math.RoundingMode +import java.time.LocalDate + +@Immutable +@Entity( + Song.TABLE, + foreignKeys = [ + ForeignKey( + entity = SongFile::class, + parentColumns = arrayOf(SongFile.COLUMN_ID), + childColumns = arrayOf(Song.COLUMN_ID), + onDelete = ForeignKey.CASCADE, + ), + ], + indices = [Index(Song.COLUMN_TITLE)], +) +data class Song( + @PrimaryKey + @ColumnInfo(COLUMN_ID) + val id: String, + @ColumnInfo(COLUMN_TITLE) + val title: String, + @ColumnInfo(COLUMN_ALBUM_ID) + val albumId: String?, + @ColumnInfo(COLUMN_TRACK_NUMBER) + val trackNumber: Int?, + @ColumnInfo(COLUMN_TRACK_TOTAL) + val trackTotal: Int?, + @ColumnInfo(COLUMN_DISC_NUMBER) + val discNumber: Int?, + @ColumnInfo(COLUMN_DISC_TOTAL) + val discTotal: Int?, + @ColumnInfo(COLUMN_DATE) + val date: LocalDate?, + @ColumnInfo(COLUMN_YEAR) + val year: Int?, + @ColumnInfo(COLUMN_DURATION) + val duration: Long, + @ColumnInfo(COLUMN_BITRATE) + val bitrate: Long?, + @ColumnInfo(COLUMN_SAMPLING_RATE) + val samplingRate: Long?, + @ColumnInfo(COLUMN_CHANNELS) + val channels: Int?, + @ColumnInfo(COLUMN_ENCODER) + val encoder: String?, + @ColumnInfo(COLUMN_DATE_MODIFIED) + val dateModified: Long, + @ColumnInfo(COLUMN_SIZE) + val size: Long, + @ColumnInfo(COLUMN_COVER_FILE) + val coverFile: String?, + @ColumnInfo(COLUMN_URI) + val uri: Uri, + @ColumnInfo(COLUMN_PATH) + val path: String, +) { + val bitrateK: Long? get() = bitrate?.let { it / 1000 } + val samplingRateK: Float? + get() = samplingRate?.let { + (it.toFloat() / 1000) + .toBigDecimal() + .setScale(1, RoundingMode.CEILING) + .toFloat() + } + + val filename get() = SimplePath(path).name + + fun createArtworkImageRequest(symphony: Symphony) = + symphony.groove.song.createArtworkImageRequest(id) + + fun toSamplingInfoString(symphony: Symphony): String? { + val values = mutableListOf() + encoder?.let { + values.add(it) + } + channels?.let { + values.add(symphony.t.XChannels(it.toString())) + } + bitrateK?.let { + values.add(buildString { + append(symphony.t.XKbps(it.toString())) + }) + } + samplingRateK?.let { + values.add(symphony.t.XKHz(it.toString())) + } + return when { + values.isNotEmpty() -> values.joinToString(", ") + else -> null + } + } + + companion object { + const val TABLE = "songs" + const val COLUMN_ID = "id" + const val COLUMN_TITLE = "title" + const val COLUMN_ALBUM_ID = "album" + const val COLUMN_TRACK_NUMBER = "track_number" + const val COLUMN_TRACK_TOTAL = "track_total" + const val COLUMN_DISC_NUMBER = "disc_number" + const val COLUMN_DISC_TOTAL = "disc_total" + const val COLUMN_DATE = "date" + const val COLUMN_YEAR = "year" + const val COLUMN_DURATION = "duration" + const val COLUMN_BITRATE = "bitrate" + const val COLUMN_SAMPLING_RATE = "sampling_rate" + const val COLUMN_CHANNELS = "channels" + const val COLUMN_ENCODER = "encoder" + const val COLUMN_DATE_MODIFIED = "date_modified" + const val COLUMN_SIZE = "size" + const val COLUMN_COVER_FILE = "cover_file" + const val COLUMN_URI = "uri" + const val COLUMN_PATH = "path" + } +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/Song.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongFile.kt similarity index 66% rename from app/src/main/java/io/github/zyrouge/symphony/services/groove/Song.kt rename to app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongFile.kt index 534f3003..bcbccc5a 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/Song.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongFile.kt @@ -1,50 +1,69 @@ -package io.github.zyrouge.symphony.services.groove +package io.github.zyrouge.symphony.services.groove.entities -import android.graphics.Bitmap -import android.graphics.BitmapFactory import android.media.MediaMetadataRetriever import android.net.Uri import android.os.Build import androidx.compose.runtime.Immutable +import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey import io.github.zyrouge.symphony.Symphony import io.github.zyrouge.symphony.utils.DocumentFileX -import io.github.zyrouge.symphony.utils.ImagePreserver import io.github.zyrouge.symphony.utils.Logger import io.github.zyrouge.symphony.utils.SimplePath import me.zyrouge.symphony.metaphony.AudioMetadataParser -import java.io.FileOutputStream -import java.math.RoundingMode import java.time.LocalDate import java.util.regex.Pattern @Immutable -@Entity("songs") -data class Song( +@Entity( + SongFile.TABLE, +) +data class SongFile( @PrimaryKey + @ColumnInfo(COLUMN_ID) val id: String, + @ColumnInfo(COLUMN_TITLE) val title: String, + @ColumnInfo(COLUMN_ALBUM) val album: String?, + @ColumnInfo(COLUMN_ARTISTS) val artists: Set, + @ColumnInfo(COLUMN_COMPOSERS) val composers: Set, + @ColumnInfo(COLUMN_ALBUM_ARTISTS) val albumArtists: Set, + @ColumnInfo(COLUMN_GENRES) val genres: Set, + @ColumnInfo(COLUMN_TRACK_NUMBER) val trackNumber: Int?, + @ColumnInfo(COLUMN_TRACK_TOTAL) val trackTotal: Int?, + @ColumnInfo(COLUMN_DISC_NUMBER) val discNumber: Int?, + @ColumnInfo(COLUMN_DISC_TOTAL) val discTotal: Int?, + @ColumnInfo(COLUMN_DATE) val date: LocalDate?, + @ColumnInfo(COLUMN_YEAR) val year: Int?, + @ColumnInfo(COLUMN_DURATION) val duration: Long, + @ColumnInfo(COLUMN_BITRATE) val bitrate: Long?, + @ColumnInfo(COLUMN_SAMPLING_RATE) val samplingRate: Long?, + @ColumnInfo(COLUMN_CHANNELS) val channels: Int?, + @ColumnInfo(COLUMN_ENCODER) val encoder: String?, + @ColumnInfo(COLUMN_DATE_MODIFIED) val dateModified: Long, + @ColumnInfo(COLUMN_SIZE) val size: Long, - val coverFile: String?, + @ColumnInfo(COLUMN_URI) val uri: Uri, + @ColumnInfo(COLUMN_PATH) val path: String, ) { data class ParseOptions( @@ -61,100 +80,73 @@ data class Song( } } - val bitrateK: Long? get() = bitrate?.let { it / 1000 } - val samplingRateK: Float? - get() = samplingRate?.let { - (it.toFloat() / 1000) - .toBigDecimal() - .setScale(1, RoundingMode.CEILING) - .toFloat() - } - - val filename get() = SimplePath(path).name - - fun createArtworkImageRequest(symphony: Symphony) = - symphony.groove.song.createArtworkImageRequest(id) - - fun toSamplingInfoString(symphony: Symphony): String? { - val values = mutableListOf() - encoder?.let { - values.add(it) - } - channels?.let { - values.add(symphony.t.XChannels(it.toString())) - } - bitrateK?.let { - values.add(buildString { - append(symphony.t.XKbps(it.toString())) - }) - } - samplingRateK?.let { - values.add(symphony.t.XKHz(it.toString())) - } - return when { - values.isNotEmpty() -> values.joinToString(", ") - else -> null - } + data class Extended( + val songFile: SongFile, + val artwork: Artwork?, + val lyrics: String?, + ) { + data class Artwork(val mimeType: String, val data: ByteArray) } companion object { + const val TABLE = "song_files" + const val COLUMN_ID = "id" + const val COLUMN_TITLE = "title" + const val COLUMN_ALBUM = "album" + const val COLUMN_ARTISTS = "artists" + const val COLUMN_COMPOSERS = "composers" + const val COLUMN_ALBUM_ARTISTS = "album_artists" + const val COLUMN_GENRES = "genres" + const val COLUMN_TRACK_NUMBER = "track_number" + const val COLUMN_TRACK_TOTAL = "track_total" + const val COLUMN_DISC_NUMBER = "disc_number" + const val COLUMN_DISC_TOTAL = "disc_total" + const val COLUMN_DATE = "date" + const val COLUMN_YEAR = "year" + const val COLUMN_DURATION = "duration" + const val COLUMN_BITRATE = "bitrate" + const val COLUMN_SAMPLING_RATE = "sampling_rate" + const val COLUMN_CHANNELS = "channels" + const val COLUMN_ENCODER = "encoder" + const val COLUMN_DATE_MODIFIED = "date_modified" + const val COLUMN_SIZE = "size" + const val COLUMN_URI = "uri" + const val COLUMN_PATH = "path" + fun parse( + id: String, path: SimplePath, file: DocumentFileX, options: ParseOptions, - ): Song { + ): Extended { if (options.symphony.settings.useMetaphony.value) { try { - val song = parseUsingMetaphony(path, file, options) - if (song != null) { - return song + val songFile = parseUsingMetaphony(id, path, file, options) + if (songFile != null) { + return songFile } } catch (err: Exception) { - Logger.error("Song", "could not parse using metaphony", err) + Logger.error("SongFile", "could not parse using metaphony", err) } } - return parseUsingMediaMetadataRetriever(path, file, options) + return parseUsingMediaMetadataRetriever(id, path, file, options) } private fun parseUsingMetaphony( + id: String, path: SimplePath, file: DocumentFileX, options: ParseOptions, - ): Song? { + ): Extended? { val symphony = options.symphony val metadata = symphony.applicationContext.contentResolver .openFileDescriptor(file.uri, "r") ?.use { AudioMetadataParser.parse(file.name, it.detachFd()) } ?: return null - val id = symphony.groove.song.idGenerator.next() - val coverFile = metadata.pictures.firstOrNull()?.let { - val extension = when (it.mimeType) { - "image/jpg", "image/jpeg" -> "jpg" - "image/png" -> "png" - else -> null - } - if (extension == null) { - return@let null - } - val quality = symphony.settings.artworkQuality.value - if (quality.maxSide == null) { - val name = "$id.$extension" - symphony.database.artworkCache.get(name).writeBytes(it.data) - return@let name - } - val bitmap = BitmapFactory.decodeByteArray(it.data, 0, it.data.size) - val name = "$id.jpg" - FileOutputStream(symphony.database.artworkCache.get(name)).use { writer -> - ImagePreserver - .resize(bitmap, quality) - .compress(Bitmap.CompressFormat.JPEG, 100, writer) - } - name + val artwork = metadata.pictures.firstOrNull()?.let { + Extended.Artwork(mimeType = it.mimeType, data = it.data) } - metadata.lyrics?.let { - symphony.database.lyricsCache.put(id, it) - } - return Song( + val songFile = SongFile( id = id, title = metadata.title ?: path.nameWithoutExtension, album = metadata.album, @@ -175,31 +167,23 @@ data class Song( encoder = metadata.encoding, dateModified = file.lastModified, size = file.size, - coverFile = coverFile, uri = file.uri, path = path.pathString, ) + return Extended(songFile = songFile, artwork = artwork, lyrics = metadata.lyrics) } fun parseUsingMediaMetadataRetriever( + id: String, path: SimplePath, file: DocumentFileX, options: ParseOptions, - ): Song { + ): Extended { val symphony = options.symphony val retriever = MediaMetadataRetriever() retriever.setDataSource(symphony.applicationContext, file.uri) - val id = symphony.groove.song.idGenerator.next() + ".mr" - val coverFile = retriever.embeddedPicture?.let { - val bitmap = BitmapFactory.decodeByteArray(it, 0, it.size) - val quality = symphony.settings.artworkQuality.value - val name = "$id.jpg" - FileOutputStream(symphony.database.artworkCache.get(name)).use { writer -> - ImagePreserver - .resize(bitmap, quality) - .compress(Bitmap.CompressFormat.JPEG, 100, writer) - } - name + val artwork = retriever.embeddedPicture?.let { + Extended.Artwork(mimeType = "_", data = it) } val title = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE) val album = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM) @@ -230,7 +214,7 @@ data class Song( .extractMetadata(MediaMetadataRetriever.METADATA_KEY_SAMPLERATE) ?.toLongOrNull() } - return Song( + val songFile = SongFile( id = id, title = title ?: path.nameWithoutExtension, album = album, @@ -251,10 +235,10 @@ data class Song( encoder = null, dateModified = file.lastModified, size = file.size, - coverFile = coverFile, uri = file.uri, path = path.pathString, ) + return Extended(songFile = songFile, artwork = artwork, lyrics = null) } private fun makeSeparatorsRegex(separators: Set): Regex { @@ -262,11 +246,11 @@ data class Song( return Regex("""(?, regex: Regex): Set { + private fun parseMultiValue(values: Set, regex: Regex): Set { val result = mutableSetOf() for (x in values) { for (y in x.trim().split(regex)) { diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongLyrics.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongLyrics.kt new file mode 100644 index 00000000..846bb1e4 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongLyrics.kt @@ -0,0 +1,33 @@ +package io.github.zyrouge.symphony.services.groove.entities + +import androidx.compose.runtime.Immutable +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey + +@Immutable +@Entity( + SongLyrics.TABLE, + foreignKeys = [ + ForeignKey( + entity = Song::class, + parentColumns = arrayOf(Song.COLUMN_ID), + childColumns = arrayOf(SongLyrics.COLUMN_SONG_FILE_ID), + onDelete = ForeignKey.CASCADE, + ), + ], +) +data class SongLyrics( + @PrimaryKey + @ColumnInfo(COLUMN_SONG_FILE_ID) + val songFileId: String, + @ColumnInfo(COLUMN_LYRICS) + val lyrics: String, +) { + companion object { + const val TABLE = "playlist_songs_mapping" + const val COLUMN_SONG_FILE_ID = "song_file_id" + const val COLUMN_LYRICS = "lyrics" + } +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/SongRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/SongRepository.kt index 0a3e06bc..2dcfbcc3 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/SongRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/SongRepository.kt @@ -3,12 +3,13 @@ package io.github.zyrouge.symphony.services.groove.repositories import android.net.Uri import androidx.core.net.toUri import io.github.zyrouge.symphony.Symphony +import io.github.zyrouge.symphony.services.groove.MediaExposer import io.github.zyrouge.symphony.services.groove.Song +import io.github.zyrouge.symphony.services.groove.SongFile import io.github.zyrouge.symphony.ui.helpers.Assets import io.github.zyrouge.symphony.ui.helpers.createHandyImageRequest import io.github.zyrouge.symphony.utils.FuzzySearchOption import io.github.zyrouge.symphony.utils.FuzzySearcher -import io.github.zyrouge.symphony.utils.KeyGenerator import io.github.zyrouge.symphony.utils.Logger import io.github.zyrouge.symphony.utils.SimpleFileSystem import io.github.zyrouge.symphony.utils.SimplePath @@ -18,6 +19,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import java.util.concurrent.ConcurrentHashMap +import kotlin.io.path.Path class SongRepository(private val symphony: Symphony) { enum class SortBy { @@ -34,9 +36,7 @@ class SongRepository(private val symphony: Symphony) { TRACK_NUMBER, } - private val cache = ConcurrentHashMap() internal val pathCache = ConcurrentHashMap() - internal val idGenerator = KeyGenerator.TimeIncremental() private val searcher = FuzzySearcher( options = listOf( FuzzySearchOption({ v -> get(v)?.title?.let { compareString(it) } }, 3), @@ -61,15 +61,12 @@ class SongRepository(private val symphony: Symphony) { System.currentTimeMillis() } - internal fun onSong(song: Song) { - cache[song.id] = song - pathCache[song.path] = song.id - explorer.addChildFile(SimplePath(song.path)).data = song.id - emitIds() - _all.update { - it + song.id + internal fun onSongFile(state: MediaExposer.SongFileState, songFile: SongFile) { + when (state) { + MediaExposer.SongFileState.New -> { + Path(songFile.path).fileName + } } - emitCount() } fun reset() { diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/KeyGenerator.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/KeyGenerator.kt index 87c2073d..d7f0a74f 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/utils/KeyGenerator.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/utils/KeyGenerator.kt @@ -3,17 +3,25 @@ package io.github.zyrouge.symphony.utils interface KeyGenerator { fun next(): String - class TimeIncremental(private var i: Int = 0, private var time: Long = 0) : KeyGenerator { + class TimeIncremental : KeyGenerator { + private var time = System.currentTimeMillis() + private var i = -1 + @Synchronized override fun next(): String { + // 256 for the giggles + if (i < 256) { + i++ + return "$time#$i" + } val now = System.currentTimeMillis() - if (now != time) { - time = now - i = 0 - } else { + if (now <= time) { i++ + return "$time#$i" } - return "$now.$i" + time = now + i = 0 + return "$now#0" } } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/Set.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/Set.kt index 9cc43ef5..143e1f6d 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/utils/Set.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/utils/Set.kt @@ -11,5 +11,5 @@ typealias ConcurrentSet = ConcurrentHashMap.KeySetView fun concurrentSetOf(vararg elements: T): ConcurrentSet = ConcurrentHashMap.newKeySet().apply { addAll(elements) } -fun concurrentSetOf(elements: Collection): ConcurrentSet = +fun concurrentSetOf(elements: Iterable): ConcurrentSet = ConcurrentHashMap.newKeySet().apply { addAll(elements) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/SimpleFileSystem.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/SimpleFileSystem.kt index cea4a96f..7e218b98 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/utils/SimpleFileSystem.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/utils/SimpleFileSystem.kt @@ -23,42 +23,37 @@ sealed class SimpleFileSystem(val parent: Folder?, val name: String) { class Folder( parent: Folder? = null, name: String = "root", - var children: ConcurrentHashMap = ConcurrentHashMap(), + private var childFolders: ConcurrentHashMap = ConcurrentHashMap(), + private var childFiles: ConcurrentHashMap = ConcurrentHashMap(), ) : SimpleFileSystem(parent, name) { - val isEmpty get() = children.isEmpty() - val childFoldersCount get() = children.values.count { it is Folder } + val isEmpty get() = childFolders.isEmpty() && childFiles.isEmpty() - fun addChildFolder(name: String): Folder { - if (children.containsKey(name)) { - throw Exception("Child '$name' already exists") - } - val child = Folder(this, name) - children[name] = child - return child + fun ensureChildFolder(name: String) = childFolders.computeIfAbsent(name) { + Folder(this, it) } fun addChildFile(name: String): File { - if (children.containsKey(name)) { + if (childFiles.containsKey(name)) { throw Exception("Child '$name' already exists") } val child = File(this, name) - children[name] = child + childFiles[name] = child return child } - fun addChildFile(path: SimplePath): File { - val parts = path.parts.toMutableList() - var parent = this - while (parts.size > 1) { - val x = parts.removeAt(0) - val found = parent.children[x] - parent = when (found) { - is Folder -> found - null -> parent.addChildFolder(x) - else -> throw Exception("Child '$x' is not a folder") - } - } - return parent.addChildFile(parts[0]) - } +// fun addChildFile(path: SimplePath): File { +// val parts = path.parts.toMutableList() +// var parent = this +// while (parts.size > 1) { +// val x = parts.removeAt(0) +// val found = parent.children[x] +// parent = when (found) { +// is Folder -> found +// null -> parent.addChildFolder(x) +// else -> throw Exception("Child '$x' is not a folder") +// } +// } +// return parent.addChildFile(parts[0]) +// } } } From 9002ac694f3625f442b031cd952a69d6a2e8d778 Mon Sep 17 00:00:00 2001 From: Zyrouge Date: Wed, 22 Jan 2025 01:36:09 +0530 Subject: [PATCH 02/15] refactor: rewrite handling after song parsing --- .../symphony/services/database/Database.kt | 51 +- .../services/database/PersistentDatabase.kt | 18 +- .../database/store/AlbumArtistMappingStore.kt | 5 +- .../database/store/AlbumSongMappingStore.kt | 5 +- .../services/database/store/AlbumStore.kt | 8 +- .../database/store/ArtistSongMappingStore.kt | 5 +- .../services/database/store/ArtistStore.kt | 11 +- .../database/store/ArtworkIndexStore.kt | 2 +- .../database/store/GenreSongMappingStore.kt | 5 +- .../services/database/store/GenreStore.kt | 12 +- .../database/store/MediaTreeFolderStore.kt | 18 +- .../database/store/MediaTreeLyricFileStore.kt | 26 + .../store/MediaTreeLyricsFileStore.kt | 26 - .../database/store/MediaTreeSongFileStore.kt | 22 +- .../store/PlaylistSongMappingStore.kt | 5 - .../services/database/store/SongFileStore.kt | 29 -- .../{SongLyricsStore.kt => SongLyricStore.kt} | 6 +- .../services/database/store/SongStore.kt | 9 +- .../symphony/services/groove/MediaExposer.kt | 445 ++++++++++++------ .../groove/entities/AlbumArtistMapping.kt | 1 + .../groove/entities/AlbumSongMapping.kt | 6 +- .../groove/entities/ArtistSongMapping.kt | 6 +- .../groove/entities/GenreSongMapping.kt | 14 +- .../groove/entities/MediaTreeFolder.kt | 14 +- ...reeLyricsFile.kt => MediaTreeLyricFile.kt} | 21 +- .../groove/entities/MediaTreeSongFile.kt | 261 +++++++++- .../groove/entities/PlaylistSongMapping.kt | 11 +- .../symphony/services/groove/entities/Song.kt | 15 +- .../services/groove/entities/SongFile.kt | 267 ----------- .../entities/{SongLyrics.kt => SongLyric.kt} | 8 +- .../groove/repositories/PlaylistRepository.kt | 2 +- .../zyrouge/symphony/utils/KeyGenerator.kt | 12 +- 32 files changed, 716 insertions(+), 630 deletions(-) create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeLyricFileStore.kt delete mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeLyricsFileStore.kt delete mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongFileStore.kt rename app/src/main/java/io/github/zyrouge/symphony/services/database/store/{SongLyricsStore.kt => SongLyricStore.kt} (76%) rename app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/{MediaTreeLyricsFile.kt => MediaTreeLyricFile.kt} (75%) delete mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongFile.kt rename app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/{SongLyrics.kt => SongLyric.kt} (81%) diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/Database.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/Database.kt index 2657eb0f..9592fbf4 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/Database.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/Database.kt @@ -5,41 +5,38 @@ import io.github.zyrouge.symphony.services.database.store.ArtworkStore import io.github.zyrouge.symphony.utils.KeyGenerator class Database(symphony: Symphony) { - private val persistent = PersistentDatabase.create(symphony) + val persistent = PersistentDatabase.create(symphony) - val albumArtistSongsIdGenerator = KeyGenerator.TimeIncremental() - val albumArtistsIdGenerator = KeyGenerator.TimeIncremental() - val albumSongsIdGenerator = KeyGenerator.TimeIncremental() - val albumsIdGenerator = KeyGenerator.TimeIncremental() - val artistSongsIdGenerator = KeyGenerator.TimeIncremental() - val artistsIdGenerator = KeyGenerator.TimeIncremental() - val mediaTreeFoldersIdGenerator = KeyGenerator.TimeIncremental() - val mediaTreeSongFilesIdGenerator = KeyGenerator.TimeIncremental() - val mediaTreeLyricsFilesIdGenerator = KeyGenerator.TimeIncremental() - val genreSongsIdGenerator = KeyGenerator.TimeIncremental() - val genreIdGenerator = KeyGenerator.TimeIncremental() - val playlistSongsIdGenerator = KeyGenerator.TimeIncremental() - val playlistsIdGenerator = KeyGenerator.TimeIncremental() - val songFilesIdGenerator = KeyGenerator.TimeIncremental() - val songLyricsIdGenerator = KeyGenerator.TimeIncremental() - val songsIdGenerator = KeyGenerator.TimeIncremental() + val albumsIdGenerator = KeyGenerator.TimeCounterRandomMix() + val albumArtistMappingIdGenerator = KeyGenerator.TimeCounterRandomMix() + val albumSongMappingIdGenerator = KeyGenerator.TimeCounterRandomMix() + val artistsIdGenerator = KeyGenerator.TimeCounterRandomMix() + val artistSongMappingIdGenerator = KeyGenerator.TimeCounterRandomMix() + val artworksIdGenerator = KeyGenerator.TimeCounterRandomMix() + val artworkIndicesIdGenerator = KeyGenerator.TimeCounterRandomMix() + val genresIdGenerator = KeyGenerator.TimeCounterRandomMix() + val genreSongMappingIdGenerator = KeyGenerator.TimeCounterRandomMix() + val mediaTreeFoldersIdGenerator = KeyGenerator.TimeCounterRandomMix() + val mediaTreeSongFilesIdGenerator = KeyGenerator.TimeCounterRandomMix() + val mediaTreeLyricFilesIdGenerator = KeyGenerator.TimeCounterRandomMix() + val playlistsIdGenerator = KeyGenerator.TimeCounterRandomMix() + val playlistSongMappingIdGenerator = KeyGenerator.TimeCounterRandomMix() + val songLyricsIdGenerator = KeyGenerator.TimeCounterRandomMix() - val albumArtistSongs get() = persistent.albumArtistSongs() - val albumArtists get() = persistent.albumArtists() - val albumSongs get() = persistent.albumSongs() val albums get() = persistent.albums() - val artistSongs get() = persistent.artistSongs() + val albumArtistMapping get() = persistent.albumArtistMapping() + val albumSongMapping get() = persistent.albumSongMapping() val artists get() = persistent.artists() - val artwork = ArtworkStore(symphony) + val artistSongMapping get() = persistent.artistSongMapping() + val artworks = ArtworkStore(symphony) val artworkIndices get() = persistent.artworkIndices() - val genreSongs get() = persistent.genreSongs() - val genre get() = persistent.genre() + val genres get() = persistent.genre() + val genreSongMapping get() = persistent.genreSongMapping() val mediaTreeFolders get() = persistent.mediaTreeFolders() + val mediaTreeLyricFiles get() = persistent.mediaTreeLyricFiles() val mediaTreeSongFiles get() = persistent.mediaTreeSongFiles() - val mediaTreeLyricsFiles get() = persistent.mediaTreeLyricsFiles() - val playlistSongs get() = persistent.playlistSongs() val playlists get() = persistent.playlists() - val songFiles get() = persistent.songFiles() + val playlistSongMapping get() = persistent.playlistSongMapping() val songLyrics get() = persistent.songLyrics() val songs get() = persistent.songs() } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/PersistentDatabase.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/PersistentDatabase.kt index e22cb425..60142025 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/PersistentDatabase.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/PersistentDatabase.kt @@ -14,12 +14,11 @@ import io.github.zyrouge.symphony.services.database.store.ArtworkIndexStore import io.github.zyrouge.symphony.services.database.store.GenreSongMappingStore import io.github.zyrouge.symphony.services.database.store.GenreStore import io.github.zyrouge.symphony.services.database.store.MediaTreeFolderStore -import io.github.zyrouge.symphony.services.database.store.MediaTreeLyricsFileStore +import io.github.zyrouge.symphony.services.database.store.MediaTreeLyricFileStore import io.github.zyrouge.symphony.services.database.store.MediaTreeSongFileStore import io.github.zyrouge.symphony.services.database.store.PlaylistSongMappingStore import io.github.zyrouge.symphony.services.database.store.PlaylistStore -import io.github.zyrouge.symphony.services.database.store.SongFileStore -import io.github.zyrouge.symphony.services.database.store.SongLyricsStore +import io.github.zyrouge.symphony.services.database.store.SongLyricStore import io.github.zyrouge.symphony.services.database.store.SongStore import io.github.zyrouge.symphony.services.groove.entities.Album import io.github.zyrouge.symphony.services.groove.entities.AlbumArtistMapping @@ -32,8 +31,7 @@ import io.github.zyrouge.symphony.services.groove.entities.GenreSongMapping import io.github.zyrouge.symphony.services.groove.entities.Playlist import io.github.zyrouge.symphony.services.groove.entities.PlaylistSongMapping import io.github.zyrouge.symphony.services.groove.entities.Song -import io.github.zyrouge.symphony.services.groove.entities.SongFile -import io.github.zyrouge.symphony.services.groove.entities.SongLyrics +import io.github.zyrouge.symphony.services.groove.entities.SongLyric import io.github.zyrouge.symphony.utils.RoomConvertors @Database( @@ -48,12 +46,11 @@ import io.github.zyrouge.symphony.utils.RoomConvertors GenreSongMapping::class, Genre::class, MediaTreeFolderStore::class, - MediaTreeLyricsFileStore::class, + MediaTreeLyricFileStore::class, MediaTreeSongFileStore::class, PlaylistSongMapping::class, Playlist::class, - SongFile::class, - SongLyrics::class, + SongLyric::class, Song::class, ], ) @@ -68,12 +65,11 @@ abstract class PersistentDatabase : RoomDatabase() { abstract fun genreSongMapping(): GenreSongMappingStore abstract fun genre(): GenreStore abstract fun mediaTreeFolders(): MediaTreeFolderStore + abstract fun mediaTreeLyricFiles(): MediaTreeLyricFileStore abstract fun mediaTreeSongFiles(): MediaTreeSongFileStore - abstract fun mediaTreeLyricsFiles(): MediaTreeLyricsFileStore abstract fun playlistSongMapping(): PlaylistSongMappingStore abstract fun playlists(): PlaylistStore - abstract fun songFiles(): SongFileStore - abstract fun songLyrics(): SongLyricsStore + abstract fun songLyrics(): SongLyricStore abstract fun songs(): SongStore companion object { diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumArtistMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumArtistMappingStore.kt index 4c76136b..d1d340ef 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumArtistMappingStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumArtistMappingStore.kt @@ -2,10 +2,11 @@ package io.github.zyrouge.symphony.services.database.store import androidx.room.Dao import androidx.room.Insert +import androidx.room.OnConflictStrategy import io.github.zyrouge.symphony.services.groove.entities.AlbumArtistMapping @Dao interface AlbumArtistMappingStore { - @Insert - suspend fun insert(vararg entities: AlbumArtistMapping) + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsert(vararg entities: AlbumArtistMapping) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumSongMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumSongMappingStore.kt index c0a4dbcd..901a7028 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumSongMappingStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumSongMappingStore.kt @@ -2,10 +2,11 @@ package io.github.zyrouge.symphony.services.database.store import androidx.room.Dao import androidx.room.Insert +import androidx.room.OnConflictStrategy import io.github.zyrouge.symphony.services.groove.entities.AlbumSongMapping @Dao interface AlbumSongMappingStore { - @Insert - suspend fun insert(vararg entities: AlbumSongMapping) + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsert(vararg entities: AlbumSongMapping) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumStore.kt index 7569ba91..692ccc2d 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumStore.kt @@ -5,7 +5,6 @@ import androidx.room.Insert import androidx.room.Query import androidx.room.Update import io.github.zyrouge.symphony.services.groove.entities.Album -import kotlinx.coroutines.flow.Flow @Dao interface AlbumStore { @@ -15,9 +14,6 @@ interface AlbumStore { @Update suspend fun update(vararg entities: Album): Int - @Query("SELECT * FROM ${Album.TABLE}") - suspend fun values(): List - - @Query("SELECT * FROM ${Album.TABLE}") - suspend fun valuesAsFlow(): Flow> + @Query("SELECT * FROM ${Album.TABLE} WHERE ${Album.COLUMN_NAME} = :name LIMIT 1") + fun findByName(name: String): Album? } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistSongMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistSongMappingStore.kt index e3c5614e..e6aa8a80 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistSongMappingStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistSongMappingStore.kt @@ -2,10 +2,11 @@ package io.github.zyrouge.symphony.services.database.store import androidx.room.Dao import androidx.room.Insert +import androidx.room.OnConflictStrategy import io.github.zyrouge.symphony.services.groove.entities.ArtistSongMapping @Dao interface ArtistSongMappingStore { - @Insert - suspend fun insert(vararg entities: ArtistSongMapping) + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsert(vararg entities: ArtistSongMapping) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistStore.kt index ab3faf42..ce2f2e40 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistStore.kt @@ -2,10 +2,10 @@ package io.github.zyrouge.symphony.services.database.store import androidx.room.Dao import androidx.room.Insert +import androidx.room.MapColumn import androidx.room.Query import androidx.room.Update import io.github.zyrouge.symphony.services.groove.entities.Artist -import kotlinx.coroutines.flow.Flow @Dao interface ArtistStore { @@ -15,9 +15,8 @@ interface ArtistStore { @Update suspend fun update(vararg entities: Artist): Int - @Query("SELECT * FROM ${Artist.TABLE}") - suspend fun values(): List - - @Query("SELECT * FROM ${Artist.TABLE}") - suspend fun valuesAsFlow(): Flow> + @Query("SELECT ${Artist.COLUMN_ID}, ${Artist.COLUMN_NAME} FROM ${Artist.TABLE} WHERE ${Artist.COLUMN_NAME} in (:names)") + fun entriesByNameNameIdMapped(names: Collection): Map< + @MapColumn(Artist.COLUMN_NAME) String, + @MapColumn(Artist.COLUMN_ID) String> } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtworkIndexStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtworkIndexStore.kt index 24c35295..42563ae9 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtworkIndexStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtworkIndexStore.kt @@ -13,5 +13,5 @@ interface ArtworkIndexStore { suspend fun upsert(vararg entities: ArtworkIndex): List @Query("SELECT * FROM ${ArtworkIndex.TABLE}") - suspend fun entriesSongIdMapped(): Map<@MapColumn(ArtworkIndex.COLUMN_SONG_ID) String, ArtworkIndex> + fun entriesSongIdMapped(): Map<@MapColumn(ArtworkIndex.COLUMN_SONG_ID) String, ArtworkIndex> } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreSongMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreSongMappingStore.kt index d36897e6..ef701ff3 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreSongMappingStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreSongMappingStore.kt @@ -2,10 +2,11 @@ package io.github.zyrouge.symphony.services.database.store import androidx.room.Dao import androidx.room.Insert +import androidx.room.OnConflictStrategy import io.github.zyrouge.symphony.services.groove.entities.GenreSongMapping @Dao interface GenreSongMappingStore { - @Insert - suspend fun insert(vararg entities: GenreSongMapping) + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsert(vararg entities: GenreSongMapping) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreStore.kt index b15a87cf..ae642287 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreStore.kt @@ -3,21 +3,13 @@ package io.github.zyrouge.symphony.services.database.store import androidx.room.Dao import androidx.room.Insert import androidx.room.Query -import androidx.room.Update import io.github.zyrouge.symphony.services.groove.entities.Genre -import kotlinx.coroutines.flow.Flow @Dao interface GenreStore { @Insert suspend fun insert(vararg entities: Genre): List - @Update - suspend fun update(vararg entities: Genre): Int - - @Query("SELECT * FROM ${Genre.TABLE}") - suspend fun values(): List - - @Query("SELECT * FROM ${Genre.TABLE}") - suspend fun valuesAsFlow(): Flow> + @Query("SELECT * FROM ${Genre.TABLE} WHERE ${Genre.COLUMN_NAME} = :name LIMIT 1") + fun findByName(name: String): Genre? } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeFolderStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeFolderStore.kt index 4f9be9a1..de4949e3 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeFolderStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeFolderStore.kt @@ -2,10 +2,10 @@ package io.github.zyrouge.symphony.services.database.store import androidx.room.Dao import androidx.room.Insert +import androidx.room.MapColumn import androidx.room.Query import androidx.room.Update import io.github.zyrouge.symphony.services.groove.entities.MediaTreeFolder -import kotlinx.coroutines.flow.Flow @Dao interface MediaTreeFolderStore { @@ -15,15 +15,15 @@ interface MediaTreeFolderStore { @Update() suspend fun update(vararg entities: MediaTreeFolder): Int - @Query("SELECT * FROM ${MediaTreeFolder.TABLE} WHERE ${MediaTreeFolder.COLUMN_PARENT_ID} = :parentId AND ${MediaTreeFolder.COLUMN_NAME} = :name LIMIT 1") - fun findByName(parentId: String?, name: String): MediaTreeFolder? + @Query("SELECT id FROM ${MediaTreeFolder.TABLE} WHERE ${MediaTreeFolder.COLUMN_PARENT_ID} = :parentId") + fun ids(parentId: String): List - @Query("SELECT * FROM ${MediaTreeFolder.TABLE} WHERE ${MediaTreeFolder.COLUMN_INTERNAL_NAME} = :internalName LIMIT 1") - fun findByInternalName(internalName: String): MediaTreeFolder? + @Query("SELECT * FROM ${MediaTreeFolder.TABLE} WHERE ${MediaTreeFolder.COLUMN_IS_HEAD} = 1 AND ${MediaTreeFolder.COLUMN_NAME} = :name") + fun findHeadByName(name: String): MediaTreeFolder? - @Query("SELECT * FROM ${MediaTreeFolder.TABLE}") - fun values(): List + @Query("SELECT * FROM ${MediaTreeFolder.TABLE} WHERE ${MediaTreeFolder.COLUMN_PARENT_ID} = :parentId AND ${MediaTreeFolder.COLUMN_NAME} = :name") + fun findByName(parentId: String, name: String): MediaTreeFolder? - @Query("SELECT * FROM ${MediaTreeFolder.TABLE}") - fun valuesAsFlow(): Flow> + @Query("SELECT * FROM ${MediaTreeFolder.TABLE} WHERE ${MediaTreeFolder.COLUMN_PARENT_ID} = :parentId") + fun entriesNameMapped(parentId: String): Map<@MapColumn(MediaTreeFolder.COLUMN_NAME) String, MediaTreeFolder> } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeLyricFileStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeLyricFileStore.kt new file mode 100644 index 00000000..78f0886a --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeLyricFileStore.kt @@ -0,0 +1,26 @@ +package io.github.zyrouge.symphony.services.database.store + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.MapColumn +import androidx.room.Query +import androidx.room.Update +import io.github.zyrouge.symphony.services.groove.entities.MediaTreeLyricFile + +@Dao +interface MediaTreeLyricFileStore { + @Insert() + suspend fun insert(vararg entities: MediaTreeLyricFile): List + + @Update() + suspend fun update(vararg entities: MediaTreeLyricFile): Int + + @Query("SELECT id FROM ${MediaTreeLyricFile.TABLE} WHERE ${MediaTreeLyricFile.COLUMN_PARENT_ID} = :parentId") + fun ids(parentId: String): List + + @Query("SELECT * FROM ${MediaTreeLyricFile.TABLE} WHERE ${MediaTreeLyricFile.COLUMN_PARENT_ID} = :parentId AND ${MediaTreeLyricFile.COLUMN_NAME} = :name LIMIT 1") + fun findByName(parentId: String, name: String): MediaTreeLyricFile? + + @Query("SELECT * FROM ${MediaTreeLyricFile.TABLE} WHERE ${MediaTreeLyricFile.COLUMN_PARENT_ID} = :parentId") + fun entriesNameMapped(parentId: String?): Map<@MapColumn(MediaTreeLyricFile.COLUMN_NAME) String, MediaTreeLyricFile> +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeLyricsFileStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeLyricsFileStore.kt deleted file mode 100644 index ea55db31..00000000 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeLyricsFileStore.kt +++ /dev/null @@ -1,26 +0,0 @@ -package io.github.zyrouge.symphony.services.database.store - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.Query -import androidx.room.Update -import io.github.zyrouge.symphony.services.groove.entities.MediaTreeLyricsFile -import kotlinx.coroutines.flow.Flow - -@Dao -interface MediaTreeLyricsFileStore { - @Insert() - fun insert(vararg entities: MediaTreeLyricsFile): List - - @Update() - fun update(vararg entities: MediaTreeLyricsFile): Int - - @Query("SELECT * FROM ${MediaTreeLyricsFile.TABLE} WHERE ${MediaTreeLyricsFile.COLUMN_PARENT_ID} = :parentId AND ${MediaTreeLyricsFile.COLUMN_NAME} = :name LIMIT 1") - fun findByName(parentId: String, name: String): MediaTreeLyricsFile? - - @Query("SELECT * FROM ${MediaTreeLyricsFile.TABLE}") - suspend fun values(): List - - @Query("SELECT * FROM ${MediaTreeLyricsFile.TABLE}") - suspend fun valuesAsFlow(): Flow> -} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeSongFileStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeSongFileStore.kt index 26fd3404..93e625dd 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeSongFileStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeSongFileStore.kt @@ -2,26 +2,28 @@ package io.github.zyrouge.symphony.services.database.store import androidx.room.Dao import androidx.room.Insert +import androidx.room.MapColumn import androidx.room.Query import androidx.room.Update -import io.github.zyrouge.symphony.services.groove.entities.MediaTreeFolder import io.github.zyrouge.symphony.services.groove.entities.MediaTreeSongFile -import kotlinx.coroutines.flow.Flow @Dao interface MediaTreeSongFileStore { @Insert - fun insert(vararg entities: MediaTreeSongFile): List + suspend fun insert(vararg entities: MediaTreeSongFile): List @Update - fun update(vararg entities: MediaTreeSongFile): Int + suspend fun update(vararg entities: MediaTreeSongFile): Int - @Query("SELECT * FROM ${MediaTreeSongFile.TABLE} WHERE ${MediaTreeSongFile.COLUMN_PARENT_ID} = :parentId AND ${MediaTreeSongFile.COLUMN_NAME} = :name LIMIT 1") - fun findByName(parentId: String, name: String): MediaTreeFolder? + @Query("DELETE FROM ${MediaTreeSongFile.TABLE} WHERE ${MediaTreeSongFile.COLUMN_ID} IN (:ids)") + suspend fun delete(ids: Collection): Int - @Query("SELECT * FROM ${MediaTreeSongFile.TABLE}") - suspend fun values(): List + @Query("SELECT id FROM ${MediaTreeSongFile.TABLE} WHERE ${MediaTreeSongFile.COLUMN_PARENT_ID} = :parentId") + fun ids(parentId: String): List - @Query("SELECT * FROM ${MediaTreeSongFile.TABLE}") - suspend fun valuesAsFlow(): Flow> + @Query("SELECT * FROM ${MediaTreeSongFile.TABLE} WHERE $${MediaTreeSongFile.COLUMN_PARENT_ID} = :parentId AND ${MediaTreeSongFile.COLUMN_NAME} = :name LIMIT 1") + fun findByName(parentId: String, name: String): MediaTreeSongFile? + + @Query("SELECT * FROM ${MediaTreeSongFile.TABLE} WHERE ${MediaTreeSongFile.COLUMN_PARENT_ID} = :parentId") + fun entriesNameMapped(parentId: String?): Map<@MapColumn(MediaTreeSongFile.COLUMN_NAME) String, MediaTreeSongFile> } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistSongMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistSongMappingStore.kt index 78a15429..39d4dead 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistSongMappingStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistSongMappingStore.kt @@ -2,15 +2,10 @@ package io.github.zyrouge.symphony.services.database.store import androidx.room.Dao import androidx.room.Insert -import androidx.room.Transaction import io.github.zyrouge.symphony.services.groove.entities.PlaylistSongMapping @Dao interface PlaylistSongMappingStore { @Insert suspend fun insert(vararg entities: PlaylistSongMapping) - - @Insert - @Transaction - suspend fun updateInTransaction(vararg entities: PlaylistSongMapping): Int } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongFileStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongFileStore.kt deleted file mode 100644 index 3b732339..00000000 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongFileStore.kt +++ /dev/null @@ -1,29 +0,0 @@ -package io.github.zyrouge.symphony.services.database.store - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.MapColumn -import androidx.room.Query -import androidx.room.Update -import io.github.zyrouge.symphony.services.groove.entities.SongFile - -@Dao -interface SongFileStore { - @Insert() - suspend fun insert(vararg entities: SongFile): List - - @Update - suspend fun update(vararg entities: SongFile): Int - - @Query("UPDATE ${SongFile.TABLE} SET ${SongFile.COLUMN_DATE_MODIFIED} = 0") - suspend fun updateDateModifiedToZero(): Int - - @Query("DELETE FROM ${SongFile.TABLE} WHERE ${SongFile.COLUMN_ID} = :id") - suspend fun delete(id: String): Int - - @Query("DELETE FROM ${SongFile.TABLE} WHERE ${SongFile.COLUMN_ID} IN (:ids)") - suspend fun delete(ids: Collection): Int - - @Query("SELECT * FROM ${SongFile.TABLE}") - suspend fun entriesPathMapped(): Map<@MapColumn(SongFile.COLUMN_PATH) String, SongFile> -} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongLyricsStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongLyricStore.kt similarity index 76% rename from app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongLyricsStore.kt rename to app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongLyricStore.kt index 297cdb68..52782769 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongLyricsStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongLyricStore.kt @@ -3,10 +3,10 @@ package io.github.zyrouge.symphony.services.database.store import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy -import io.github.zyrouge.symphony.services.groove.entities.SongLyrics +import io.github.zyrouge.symphony.services.groove.entities.SongLyric @Dao -interface SongLyricsStore { +interface SongLyricStore { @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun upsert(vararg entities: SongLyrics) + suspend fun upsert(vararg entities: SongLyric) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongStore.kt index 4428f01c..0e430bf2 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongStore.kt @@ -5,7 +5,6 @@ import androidx.room.Insert import androidx.room.Query import androidx.room.Update import io.github.zyrouge.symphony.services.groove.entities.Song -import kotlinx.coroutines.flow.Flow @Dao interface SongStore { @@ -21,9 +20,9 @@ interface SongStore { @Query("DELETE FROM ${Song.TABLE} WHERE ${Song.COLUMN_ID} IN (:ids)") suspend fun delete(ids: Collection): Int - @Query("SELECT * FROM ${Song.TABLE}") - suspend fun values(): List + @Query("SELECT * FROM ${Song.TABLE} WHERE ${Song.COLUMN_PATH} = :path LIMIT 1") + fun findByPath(path: String): Song? - @Query("SELECT * FROM ${Song.TABLE}") - suspend fun valuesAsFlow(): Flow> + @Query("SELECT ${Song.COLUMN_ID} FROM ${Song.TABLE}") + fun ids(): List } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/MediaExposer.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/MediaExposer.kt index 8a64a4dc..ab775a1b 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/MediaExposer.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/MediaExposer.kt @@ -3,12 +3,19 @@ package io.github.zyrouge.symphony.services.groove import android.graphics.Bitmap import android.graphics.BitmapFactory import io.github.zyrouge.symphony.Symphony +import io.github.zyrouge.symphony.services.groove.entities.Album +import io.github.zyrouge.symphony.services.groove.entities.AlbumArtistMapping +import io.github.zyrouge.symphony.services.groove.entities.AlbumSongMapping +import io.github.zyrouge.symphony.services.groove.entities.Artist +import io.github.zyrouge.symphony.services.groove.entities.ArtistSongMapping import io.github.zyrouge.symphony.services.groove.entities.ArtworkIndex +import io.github.zyrouge.symphony.services.groove.entities.Genre +import io.github.zyrouge.symphony.services.groove.entities.GenreSongMapping import io.github.zyrouge.symphony.services.groove.entities.MediaTreeFolder -import io.github.zyrouge.symphony.services.groove.entities.MediaTreeLyricsFile +import io.github.zyrouge.symphony.services.groove.entities.MediaTreeLyricFile import io.github.zyrouge.symphony.services.groove.entities.MediaTreeSongFile -import io.github.zyrouge.symphony.services.groove.entities.SongFile -import io.github.zyrouge.symphony.services.groove.entities.SongLyrics +import io.github.zyrouge.symphony.services.groove.entities.Song +import io.github.zyrouge.symphony.services.groove.entities.SongLyric import io.github.zyrouge.symphony.utils.ActivityUtils import io.github.zyrouge.symphony.utils.ConcurrentSet import io.github.zyrouge.symphony.utils.DocumentFileX @@ -26,6 +33,8 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import java.io.FileOutputStream import java.util.concurrent.ConcurrentHashMap +import kotlin.math.max +import kotlin.math.min import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds @@ -47,7 +56,7 @@ class MediaExposer(private val symphony: Symphony) { DocumentFileX.fromTreeUri(context, x)?.let { val path = SimplePath(DocumentFileX.getParentPathOfTreeUri(x) ?: it.name) with(Dispatchers.IO) { - scanner.scanMediaTree(path, scanner.root, it) + scanner.scanMediaFolder(path, scanner.rootFolder, it) } } } @@ -61,178 +70,314 @@ class MediaExposer(private val symphony: Symphony) { private data class Scanner( val symphony: Symphony, - val songCache: ConcurrentHashMap, + val rootFolder: MediaTreeFolder, + val folderStaleIds: ConcurrentSet, + val songFileStaleIds: ConcurrentSet, + val lyricFileStaleIds: ConcurrentSet, val songStaleIds: ConcurrentSet, val artworkIndexCache: ConcurrentHashMap, val artworkStaleFiles: ConcurrentSet, - val root: MediaTreeFolder, val filter: MediaFilter, - val songParseOptions: SongFile.ParseOptions, + val songParseOptions: MediaTreeSongFile.ParseOptions, ) { - suspend fun scanMediaTree(path: SimplePath, parent: MediaTreeFolder, xfile: DocumentFileX) { - try { - if (!filter.isWhitelisted(path.pathString)) { - return - } - val exParent = symphony.database.mediaTreeFolders.findByName(parent.id, xfile.name) - if (exParent?.dateModified == xfile.lastModified) { - return - } - val nParent = exParent ?: MediaTreeFolder( - id = symphony.database.mediaTreeFoldersIdGenerator.next(), - parentId = parent.id, - internalName = null, - name = xfile.name, - uri = xfile.uri, - dateModified = 0, // change it after scanning is done - ) - if (exParent == null) { - symphony.database.mediaTreeFolders.insert(nParent) - } - coroutineScope { - xfile.list().map { - val nPath = path.join(it.name) - async { - when { - it.isDirectory -> scanMediaTree(nPath, nParent, it) - else -> scanMediaFile(nPath, nParent, it) - } - } - }.awaitAll() - } - symphony.database.mediaTreeFolders.update( - nParent.copy(dateModified = xfile.lastModified), - ) - } catch (err: Exception) { - Logger.error("MediaExposer", "scan media tree failed", err) + suspend fun scanMediaFolder( + path: SimplePath, + parent: MediaTreeFolder, + document: DocumentFileX, + ) { + if (!filter.isWhitelisted(path.pathString)) { + return } + val exFolder = symphony.database.mediaTreeFolders.findByName(parent.id, document.name) + scanMediaFolder(path, parent, exFolder, document) } - suspend fun scanMediaFile(path: SimplePath, parent: MediaTreeFolder, xfile: DocumentFileX) { - try { - when { - path.extension == "lrc" -> scanLrcFile(path, parent, xfile) - xfile.mimeType.startsWith("audio/") -> scanAudioFile(path, parent, xfile) - } - } catch (err: Exception) { - Logger.error("MediaExposer", "scan media file failed", err) + suspend fun scanMediaFolder( + path: SimplePath, + parent: MediaTreeFolder, + exFolder: MediaTreeFolder?, + document: DocumentFileX, + ) { + if (!filter.isWhitelisted(path.pathString)) { + return } - } - - suspend fun scanAudioFile(path: SimplePath, parent: MediaTreeFolder, xfile: DocumentFileX) { - val exSongFile = songCache[path.pathString] - val exArtworkIndex = artworkIndexCache[exSongFile?.id] - val skipArtworkParsing = exArtworkIndex != null && exArtworkIndex.let { - exArtworkIndex.file == null || artworkStaleFiles.contains(exArtworkIndex.file) + if (exFolder?.dateModified == document.lastModified) { + folderStaleIds.remove(exFolder.id) + return } - val skipParsing = skipArtworkParsing && exSongFile?.dateModified == xfile.lastModified - val state: SongFileState - val songFile: SongFile - val artworkIndex: ArtworkIndex - when { - skipParsing -> { - state = SongFileState.Existing - songFile = exSongFile - artworkIndex = exArtworkIndex - } + val folder = exFolder ?: MediaTreeFolder( + id = symphony.database.mediaTreeFoldersIdGenerator.next(), + parentId = parent.id, + name = document.name, + isHead = false, + uri = document.uri, + dateModified = 0, // updated after scan + ) + folderStaleIds.remove(folder.id) + if (exFolder == null) { + symphony.database.mediaTreeFolders.insert(folder) + } + scanMediaFolderChildren(path, folder, document) + symphony.database.mediaTreeFolders.update( + folder.copy(dateModified = document.lastModified), + ) + } - else -> { - state = when { - exSongFile != null -> SongFileState.Updated - else -> SongFileState.New - } - val id = exSongFile?.id ?: symphony.database.songsIdGenerator.next() - val extended = SongFile.parse(id, path, xfile, songParseOptions) - songFile = extended.songFile - val artworkFile = extended.artwork?.let { - val extension = when (it.mimeType) { - "image/jpg", "image/jpeg" -> "jpg" - "image/png" -> "png" - "_" -> "_" - else -> null - } - if (extension == null) { - return@let null - } - val quality = symphony.settings.artworkQuality.value - if (quality.maxSide == null && extension != "_") { - val name = "$id.$extension" - symphony.database.artwork.get(name).writeBytes(it.data) - return@let name + private suspend fun scanMediaFolderChildren( + path: SimplePath, + folder: MediaTreeFolder, + document: DocumentFileX, + ) { + coroutineScope { + val folders = symphony.database.mediaTreeFolders.entriesNameMapped(folder.id) + val songFiles = symphony.database.mediaTreeSongFiles.entriesNameMapped(folder.id) + val lyricFiles = symphony.database.mediaTreeLyricFiles.entriesNameMapped(folder.id) + document.list().mapNotNull { + val nPath = path.join(it.name) + if (it.isDirectory) { + return@mapNotNull async { + scanMediaFolder(nPath, folder, folders[it.name], it) } - val bitmap = BitmapFactory.decodeByteArray(it.data, 0, it.data.size) - val name = "$id.jpg" - FileOutputStream(symphony.database.artwork.get(name)).use { writer -> - ImagePreserver - .resize(bitmap, quality) - .compress(Bitmap.CompressFormat.JPEG, 100, writer) + } + if (nPath.extension == "lrc") { + return@mapNotNull async { + scanLyricFile(nPath, folder, lyricFiles[it.name], document) } - name } - artworkIndex = ArtworkIndex(songId = id, file = artworkFile) - extended.lyrics?.let { - symphony.database.songLyrics.upsert(SongLyrics(id, it)) + if (document.mimeType.startsWith("audio/")) { + return@mapNotNull async { + scanSongFile(nPath, folder, songFiles[it.name], document) + } } - symphony.database.songFiles.update(songFile) - } + null + }.awaitAll() } - if (!skipArtworkParsing || !skipParsing) { - symphony.database.artworkIndices.upsert(artworkIndex) + } + + suspend fun scanSongFile( + path: SimplePath, + parent: MediaTreeFolder, + exFile: MediaTreeSongFile?, + document: DocumentFileX, + ) { + val exArtworkIndex = artworkIndexCache[exFile?.id] + val skipArtworkParsing = exArtworkIndex != null && exArtworkIndex.let { + exArtworkIndex.file == null || artworkStaleFiles.contains(exArtworkIndex.file) + } + if (skipArtworkParsing && exFile?.dateModified == document.lastModified) { + songFileStaleIds.remove(exFile.id) + exArtworkIndex.file?.let { artworkStaleFiles.remove(it) } + return } + val id = exFile?.id ?: symphony.database.mediaTreeSongFilesIdGenerator.next() + val extended = MediaTreeSongFile.parse(path, parent, document, id, songParseOptions) + val file = extended.file + songFileStaleIds.remove(file.id) + val artworkFile = extended.artwork?.let { + val extension = when (it.mimeType) { + "image/jpg", "image/jpeg" -> "jpg" + "image/png" -> "png" + "_" -> "_" + else -> null + } + if (extension == null) { + return@let null + } + val quality = symphony.settings.artworkQuality.value + if (quality.maxSide == null && extension != "_") { + val name = "$id.$extension" + symphony.database.artworks.get(name).writeBytes(it.data) + return@let name + } + val bitmap = BitmapFactory.decodeByteArray(it.data, 0, it.data.size) + val name = "$id.jpg" + FileOutputStream(symphony.database.artworks.get(name)).use { writer -> + ImagePreserver + .resize(bitmap, quality) + .compress(Bitmap.CompressFormat.JPEG, 100, writer) + } + name + } + val artworkIndex = ArtworkIndex(songId = id, file = artworkFile) artworkIndex.file?.let { artworkStaleFiles.remove(it) } - songStaleIds.remove(songFile.id) - val exFile = symphony.database.mediaTreeSongFiles.findByName(parent.id, xfile.name) - val file = MediaTreeSongFile( - id = exFile?.id ?: symphony.database.mediaTreeSongFilesIdGenerator.next(), - parentId = parent.id, - songFileId = songFile.id, - name = xfile.name, - uri = xfile.uri, - dateModified = xfile.lastModified, - ) + symphony.database.mediaTreeSongFiles.update(file) + if (exArtworkIndex?.file != artworkIndex.file) { + symphony.database.artworkIndices.upsert(artworkIndex) + } + extended.lyrics?.let { + symphony.database.songLyrics.upsert(SongLyric(id, it)) + } when { exFile == null -> symphony.database.mediaTreeSongFiles.insert(file) else -> symphony.database.mediaTreeSongFiles.update(file) } - if (songFile.duration.milliseconds < symphony.settings.minSongDuration.value.seconds) { + if (file.duration.milliseconds < symphony.settings.minSongDuration.value.seconds) { return } - symphony.groove.exposer.emitSongFile(state, songFile) + val exSong = symphony.database.songs.findByPath(file.path) + val song = when { + exSong != null -> exSong.copy( + title = file.title, + trackNumber = file.trackNumber, + trackTotal = file.trackTotal, + discNumber = file.discNumber, + discTotal = file.discTotal, + date = file.date, + year = file.year, + duration = file.duration, + bitrate = file.bitrate, + samplingRate = file.samplingRate, + channels = file.channels, + encoder = file.encoder, + dateModified = file.dateModified, + size = file.size, + uri = file.uri, + path = file.path, + ) + + else -> Song( + id = id, + title = file.title, + trackNumber = file.trackNumber, + trackTotal = file.trackTotal, + discNumber = file.discNumber, + discTotal = file.discTotal, + date = file.date, + year = file.year, + duration = file.duration, + bitrate = file.bitrate, + samplingRate = file.samplingRate, + channels = file.channels, + encoder = file.encoder, + dateModified = file.dateModified, + size = file.size, + uri = file.uri, + path = file.path, + ) + } + when { + exSong == null -> symphony.database.songs.insert(song) + else -> symphony.database.songs.update(song) + } + val exAlbum = file.album?.let { albumName -> + symphony.database.albums.findByName(albumName) + } + val album = file.album?.let { albumName -> + val songYear = file.year ?: file.date?.year + when { + exAlbum != null -> { + val startYear = when { + songYear == null -> exAlbum.startYear + exAlbum.startYear == null -> songYear + else -> min(songYear, exAlbum.startYear) + } + val endYear = when { + songYear == null -> exAlbum.endYear + exAlbum.endYear == null -> songYear + else -> max(songYear, exAlbum.endYear) + } + exAlbum.copy(startYear = startYear, endYear = endYear) + } + + else -> Album( + id = symphony.database.albumsIdGenerator.next(), + name = albumName, + startYear = songYear, + endYear = songYear, + ) + } + } + if (album != null) { + when { + exAlbum == null -> symphony.database.albums.insert(album) + else -> symphony.database.albums.update(album) + } + val albumSongMapping = AlbumSongMapping(albumId = album.id, songId = id) + symphony.database.albumSongMapping.upsert(albumSongMapping) + } + val artists = file.artists + file.albumArtists + val artistsToBeInserted = mutableListOf() + val artistSongMappingsToBeUpserted = mutableListOf() + val albumArtistMappingsToBeUpserted = mutableMapOf() + val exArtistIds = symphony.database.artists.entriesByNameNameIdMapped(artists) + val artistIds = mutableMapOf() + artistIds.putAll(exArtistIds) + for (artistName in artists) { + val exArtistId = exArtistIds[artistName] + val artistId = exArtistId ?: symphony.database.artistsIdGenerator.next() + if (exArtistId == null) { + val artist = Artist(id = artistId, name = artistName) + artistIds[artistName] = artist.id + artistsToBeInserted.add(artist) + } + val artistSongMapping = ArtistSongMapping(artistId = artistId, songId = id) + artistSongMappingsToBeUpserted.add(artistSongMapping) + if (album != null) { + val albumArtistMapping = AlbumArtistMapping( + albumId = album.id, + artistId = artistId, + isAlbumArtist = file.albumArtists.contains(artistName), + ) + albumArtistMappingsToBeUpserted[artistName] = albumArtistMapping + } + } + symphony.database.artists.insert(*artistsToBeInserted.toTypedArray()) + symphony.database.artistSongMapping.upsert(*artistSongMappingsToBeUpserted.toTypedArray()) + symphony.database.albumArtistMapping.upsert(*albumArtistMappingsToBeUpserted.values.toTypedArray()) + val genreSongMappingsToBeUpserted = mutableListOf() + for (genreName in file.genres) { + val exGenre = symphony.database.genres.findByName(genreName) + val genre = when { + exGenre != null -> exGenre + else -> Genre( + id = symphony.database.genresIdGenerator.next(), + name = genreName, + ) + } + if (exGenre == null) { + symphony.database.genres.insert(genre) + } + val genreSongMapping = GenreSongMapping(genreId = genre.id, songId = id) + genreSongMappingsToBeUpserted.add(genreSongMapping) + } + symphony.database.genreSongMapping.upsert(*genreSongMappingsToBeUpserted.toTypedArray()) } - fun scanLrcFile( + suspend fun scanLyricFile( @Suppress("Unused") path: SimplePath, parent: MediaTreeFolder, - xfile: DocumentFileX, + exFile: MediaTreeLyricFile?, + document: DocumentFileX, ) { - val exFile = symphony.database.mediaTreeLyricsFiles.findByName(parent.id, xfile.name) - if (exFile?.dateModified == xfile.lastModified) { + if (exFile?.dateModified == document.lastModified) { + lyricFileStaleIds.remove(exFile.id) return } - val file = MediaTreeLyricsFile( - id = exFile?.id ?: symphony.database.mediaTreeLyricsFilesIdGenerator.next(), + val file = MediaTreeLyricFile( + id = exFile?.id ?: symphony.database.mediaTreeLyricFilesIdGenerator.next(), parentId = parent.id, - name = xfile.name, - uri = xfile.uri, - dateModified = xfile.lastModified, + name = document.name, + dateModified = document.lastModified, + uri = document.uri, ) + lyricFileStaleIds.remove(file.id) when { - exFile == null -> symphony.database.mediaTreeLyricsFiles.insert(file) - else -> symphony.database.mediaTreeLyricsFiles.update(file) + exFile == null -> symphony.database.mediaTreeLyricFiles.insert(file) + else -> symphony.database.mediaTreeLyricFiles.update(file) } } suspend fun cleanup() { try { - symphony.database.songFiles.delete(songStaleIds) + symphony.database.mediaTreeSongFiles.delete(songFileStaleIds) } catch (err: Exception) { Logger.warn("MediaExposer", "trimming song files failed", err) } for (x in artworkStaleFiles) { try { - symphony.database.artwork.get(x).delete() + symphony.database.artworks.get(x).delete() } catch (err: Exception) { Logger.warn("MediaExposer", "deleting artwork failed", err) } @@ -246,47 +391,43 @@ class MediaExposer(private val symphony: Symphony) { symphony.settings.blacklistFolders.value.toSortedSet(), symphony.settings.whitelistFolders.value.toSortedSet() ) - val songEntries = symphony.database.songFiles.entriesPathMapped() + val rootFolder = getTreeRootFolder(symphony) + val folderIds = symphony.database.mediaTreeFolders.ids(rootFolder.id) + val songFileIds = symphony.database.mediaTreeSongFiles.ids(rootFolder.id) + val lyricFileIds = symphony.database.mediaTreeLyricFiles.ids(rootFolder.id) + val songIds = symphony.database.songs.ids() return Scanner( symphony = symphony, - songCache = ConcurrentHashMap(songEntries), - songStaleIds = concurrentSetOf(songEntries.keys), + rootFolder = rootFolder, + folderStaleIds = concurrentSetOf(folderIds), + songFileStaleIds = concurrentSetOf(songFileIds), + lyricFileStaleIds = concurrentSetOf(lyricFileIds), + songStaleIds = concurrentSetOf(songIds), artworkIndexCache = ConcurrentHashMap(symphony.database.artworkIndices.entriesSongIdMapped()), - artworkStaleFiles = concurrentSetOf(symphony.database.artwork.all()), - root = getTreeRootFolder(symphony), + artworkStaleFiles = concurrentSetOf(symphony.database.artworks.all()), filter = filter, - songParseOptions = SongFile.ParseOptions.create(symphony), + songParseOptions = MediaTreeSongFile.ParseOptions.create(symphony), ) } private suspend fun getTreeRootFolder(symphony: Symphony): MediaTreeFolder { - symphony.database.mediaTreeFolders.findByInternalName(MEDIA_TREE_ROOT_NAME)?.let { + symphony.database.mediaTreeFolders.findHeadByName(MEDIA_TREE_ROOT_NAME)?.let { return it } - val folder = MediaTreeFolder( + val root = MediaTreeFolder( id = symphony.database.mediaTreeFoldersIdGenerator.next(), parentId = null, - internalName = MEDIA_TREE_ROOT_NAME, name = MEDIA_TREE_ROOT_NAME, - uri = null, + isHead = true, dateModified = 0, + uri = null, ) - symphony.database.mediaTreeFolders.insert(folder) - return folder + symphony.database.mediaTreeFolders.insert(root) + return root } } } - enum class SongFileState { - New, - Updated, - Existing, - } - - private fun emitSongFile(state: SongFileState, songFile: SongFile) { - symphony.groove.song.onSongFile(state, songFile) - } - private fun emitFinish() { symphony.groove.playlist.onScanFinish() } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/AlbumArtistMapping.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/AlbumArtistMapping.kt index 802286d3..3f88b571 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/AlbumArtistMapping.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/AlbumArtistMapping.kt @@ -9,6 +9,7 @@ import androidx.room.Index @Immutable @Entity( AlbumArtistMapping.TABLE, + primaryKeys = [AlbumArtistMapping.COLUMN_ALBUM_ID, AlbumArtistMapping.COLUMN_ARTIST_ID], foreignKeys = [ ForeignKey( entity = Album::class, diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/AlbumSongMapping.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/AlbumSongMapping.kt index 19955e41..e6cc4ed8 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/AlbumSongMapping.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/AlbumSongMapping.kt @@ -9,6 +9,7 @@ import androidx.room.Index @Immutable @Entity( AlbumSongMapping.TABLE, + primaryKeys = [AlbumSongMapping.COLUMN_ALBUM_ID, AlbumSongMapping.COLUMN_SONG_ID], foreignKeys = [ ForeignKey( entity = Album::class, @@ -23,7 +24,10 @@ import androidx.room.Index onDelete = ForeignKey.CASCADE, ), ], - indices = [Index(AlbumSongMapping.COLUMN_ALBUM_ID)], + indices = [ + Index(AlbumSongMapping.COLUMN_ALBUM_ID), + Index(AlbumSongMapping.COLUMN_SONG_ID), + ], ) data class AlbumSongMapping( @ColumnInfo(COLUMN_ALBUM_ID) diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/ArtistSongMapping.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/ArtistSongMapping.kt index dd15a6eb..4cc6fb3e 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/ArtistSongMapping.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/ArtistSongMapping.kt @@ -9,6 +9,7 @@ import androidx.room.Index @Immutable @Entity( ArtistSongMapping.TABLE, + primaryKeys = [ArtistSongMapping.COLUMN_ARTIST_ID, ArtistSongMapping.COLUMN_SONG_ID], foreignKeys = [ ForeignKey( entity = AlbumArtistMapping::class, @@ -23,7 +24,10 @@ import androidx.room.Index onDelete = ForeignKey.CASCADE, ), ], - indices = [Index(ArtistSongMapping.COLUMN_ARTIST_ID)], + indices = [ + Index(ArtistSongMapping.COLUMN_ARTIST_ID), + Index(ArtistSongMapping.COLUMN_SONG_ID), + ], ) data class ArtistSongMapping( @ColumnInfo(COLUMN_ARTIST_ID) diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/GenreSongMapping.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/GenreSongMapping.kt index 4458ba36..4ca02b55 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/GenreSongMapping.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/GenreSongMapping.kt @@ -9,11 +9,12 @@ import androidx.room.Index @Immutable @Entity( GenreSongMapping.TABLE, + primaryKeys = [GenreSongMapping.COLUMN_GENRE_ID, GenreSongMapping.COLUMN_SONG_ID], foreignKeys = [ ForeignKey( entity = AlbumArtistMapping::class, parentColumns = arrayOf(Genre.COLUMN_ID), - childColumns = arrayOf(GenreSongMapping.GENRE_ID), + childColumns = arrayOf(GenreSongMapping.COLUMN_GENRE_ID), onDelete = ForeignKey.CASCADE, ), ForeignKey( @@ -23,17 +24,20 @@ import androidx.room.Index onDelete = ForeignKey.CASCADE, ), ], - indices = [Index(GenreSongMapping.GENRE_ID)], + indices = [ + Index(GenreSongMapping.COLUMN_GENRE_ID), + Index(GenreSongMapping.COLUMN_SONG_ID), + ], ) data class GenreSongMapping( - @ColumnInfo(GENRE_ID) + @ColumnInfo(COLUMN_GENRE_ID) val genreId: String, @ColumnInfo(COLUMN_SONG_ID) val songId: String, ) { companion object { - const val TABLE = "artist_songs_mapping" - const val GENRE_ID = "genre_id" + const val TABLE = "genre_songs_mapping" + const val COLUMN_GENRE_ID = "genre_id" const val COLUMN_SONG_ID = "song_id" } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/MediaTreeFolder.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/MediaTreeFolder.kt index 2a2ff735..dd701745 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/MediaTreeFolder.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/MediaTreeFolder.kt @@ -21,8 +21,8 @@ import androidx.room.PrimaryKey ], indices = [ Index(MediaTreeFolder.COLUMN_PARENT_ID), - Index(MediaTreeFolder.COLUMN_INTERNAL_NAME, unique = true), Index(MediaTreeFolder.COLUMN_NAME), + Index(MediaTreeFolder.COLUMN_IS_HEAD), ] ) data class MediaTreeFolder( @@ -31,22 +31,22 @@ data class MediaTreeFolder( val id: String, @ColumnInfo(COLUMN_PARENT_ID) val parentId: String?, - @ColumnInfo(COLUMN_INTERNAL_NAME) - val internalName: String?, @ColumnInfo(COLUMN_NAME) val name: String, - @ColumnInfo(COLUMN_URI) - val uri: Uri?, + @ColumnInfo(COLUMN_IS_HEAD) + val isHead: Boolean, @ColumnInfo(COLUMN_DATE_MODIFIED) val dateModified: Long, + @ColumnInfo(COLUMN_URI) + val uri: Uri?, ) { companion object { const val TABLE = "media_tree_folders" const val COLUMN_ID = "id" const val COLUMN_PARENT_ID = "parent_id" - const val COLUMN_INTERNAL_NAME = "internal_name" + const val COLUMN_IS_HEAD = "is_head" const val COLUMN_NAME = "name" - const val COLUMN_URI = "uri" const val COLUMN_DATE_MODIFIED = "date_modified" + const val COLUMN_URI = "uri" } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/MediaTreeLyricsFile.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/MediaTreeLyricFile.kt similarity index 75% rename from app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/MediaTreeLyricsFile.kt rename to app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/MediaTreeLyricFile.kt index 1007d1d7..1f66f3d0 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/MediaTreeLyricsFile.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/MediaTreeLyricFile.kt @@ -10,27 +10,27 @@ import androidx.room.PrimaryKey @Immutable @Entity( - MediaTreeLyricsFile.TABLE, + MediaTreeLyricFile.TABLE, foreignKeys = [ ForeignKey( entity = MediaTreeFolder::class, parentColumns = arrayOf(MediaTreeFolder.COLUMN_ID), - childColumns = arrayOf(MediaTreeLyricsFile.COLUMN_PARENT_ID), + childColumns = arrayOf(MediaTreeLyricFile.COLUMN_PARENT_ID), onDelete = ForeignKey.CASCADE, ), ForeignKey( entity = Playlist::class, parentColumns = arrayOf(Song.COLUMN_ID), - childColumns = arrayOf(MediaTreeLyricsFile.COLUMN_NAME), + childColumns = arrayOf(MediaTreeLyricFile.COLUMN_NAME), onDelete = ForeignKey.CASCADE, ), ], indices = [ - Index(MediaTreeLyricsFile.COLUMN_PARENT_ID), - Index(MediaTreeLyricsFile.COLUMN_NAME), + Index(MediaTreeLyricFile.COLUMN_PARENT_ID), + Index(MediaTreeLyricFile.COLUMN_NAME), ], ) -data class MediaTreeLyricsFile( +data class MediaTreeLyricFile( @PrimaryKey @ColumnInfo(COLUMN_ID) val id: String, @@ -38,17 +38,18 @@ data class MediaTreeLyricsFile( val parentId: String, @ColumnInfo(COLUMN_NAME) val name: String, - @ColumnInfo(COLUMN_URI) - val uri: Uri, @ColumnInfo(COLUMN_DATE_MODIFIED) val dateModified: Long, + @ColumnInfo(COLUMN_URI) + val uri: Uri, ) { companion object { - const val TABLE = "media_tree_song_files" + const val TABLE = "media_tree_lyric_files" const val COLUMN_ID = "id" + const val COLUMN_ROOT_ID = "root_id" const val COLUMN_PARENT_ID = "parent_id" const val COLUMN_NAME = "name" - const val COLUMN_URI = "uri" const val COLUMN_DATE_MODIFIED = "date_modified" + const val COLUMN_URI = "uri" } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/MediaTreeSongFile.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/MediaTreeSongFile.kt index c3aacb75..1942d820 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/MediaTreeSongFile.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/MediaTreeSongFile.kt @@ -1,12 +1,21 @@ package io.github.zyrouge.symphony.services.groove.entities +import android.media.MediaMetadataRetriever import android.net.Uri +import android.os.Build import androidx.compose.runtime.Immutable import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.ForeignKey import androidx.room.Index import androidx.room.PrimaryKey +import io.github.zyrouge.symphony.Symphony +import io.github.zyrouge.symphony.utils.DocumentFileX +import io.github.zyrouge.symphony.utils.Logger +import io.github.zyrouge.symphony.utils.SimplePath +import me.zyrouge.symphony.metaphony.AudioMetadataParser +import java.time.LocalDate +import java.util.regex.Pattern @Immutable @Entity( @@ -18,12 +27,6 @@ import androidx.room.PrimaryKey childColumns = arrayOf(MediaTreeSongFile.COLUMN_PARENT_ID), onDelete = ForeignKey.CASCADE, ), - ForeignKey( - entity = SongFile::class, - parentColumns = arrayOf(SongFile.COLUMN_ID), - childColumns = arrayOf(MediaTreeSongFile.COLUMN_SONG_FILE_ID), - onDelete = ForeignKey.CASCADE, - ), ], indices = [ Index(MediaTreeSongFile.COLUMN_PARENT_ID), @@ -36,22 +39,256 @@ data class MediaTreeSongFile( val id: String, @ColumnInfo(COLUMN_PARENT_ID) val parentId: String, - @ColumnInfo(COLUMN_SONG_FILE_ID) - val songFileId: String, @ColumnInfo(COLUMN_NAME) val name: String, - @ColumnInfo(COLUMN_URI) - val uri: Uri, + @ColumnInfo(COLUMN_TITLE) + val title: String, + @ColumnInfo(COLUMN_ALBUM) + val album: String?, + @ColumnInfo(COLUMN_ARTISTS) + val artists: Set, + @ColumnInfo(COLUMN_COMPOSERS) + val composers: Set, + @ColumnInfo(COLUMN_ALBUM_ARTISTS) + val albumArtists: Set, + @ColumnInfo(COLUMN_GENRES) + val genres: Set, + @ColumnInfo(COLUMN_TRACK_NUMBER) + val trackNumber: Int?, + @ColumnInfo(COLUMN_TRACK_TOTAL) + val trackTotal: Int?, + @ColumnInfo(COLUMN_DISC_NUMBER) + val discNumber: Int?, + @ColumnInfo(COLUMN_DISC_TOTAL) + val discTotal: Int?, + @ColumnInfo(COLUMN_DATE) + val date: LocalDate?, + @ColumnInfo(COLUMN_YEAR) + val year: Int?, + @ColumnInfo(COLUMN_DURATION) + val duration: Long, + @ColumnInfo(COLUMN_BITRATE) + val bitrate: Long?, + @ColumnInfo(COLUMN_SAMPLING_RATE) + val samplingRate: Long?, + @ColumnInfo(COLUMN_CHANNELS) + val channels: Int?, + @ColumnInfo(COLUMN_ENCODER) + val encoder: String?, @ColumnInfo(COLUMN_DATE_MODIFIED) val dateModified: Long, + @ColumnInfo(COLUMN_SIZE) + val size: Long, + @ColumnInfo(COLUMN_URI) + val uri: Uri, + @ColumnInfo(COLUMN_REAL_PATH) + val path: String, ) { + data class Extended( + val file: MediaTreeSongFile, + val artwork: Artwork?, + val lyrics: String?, + ) { + data class Artwork(val mimeType: String, val data: ByteArray) + } + + data class ParseOptions( + val symphony: Symphony, + val artistSeparatorRegex: Regex, + val genreSeparatorRegex: Regex, + ) { + companion object { + fun create(symphony: Symphony) = ParseOptions( + symphony = symphony, + artistSeparatorRegex = makeSeparatorsRegex(symphony.settings.artistTagSeparators.value), + genreSeparatorRegex = makeSeparatorsRegex(symphony.settings.genreTagSeparators.value), + ) + } + } + companion object { const val TABLE = "media_tree_song_files" const val COLUMN_ID = "id" const val COLUMN_PARENT_ID = "parent_id" - const val COLUMN_SONG_FILE_ID = "song_file_id" const val COLUMN_NAME = "name" - const val COLUMN_URI = "uri" + const val COLUMN_TITLE = "title" + const val COLUMN_ALBUM = "album" + const val COLUMN_ARTISTS = "artists" + const val COLUMN_COMPOSERS = "composers" + const val COLUMN_ALBUM_ARTISTS = "album_artists" + const val COLUMN_GENRES = "genres" + const val COLUMN_TRACK_NUMBER = "track_number" + const val COLUMN_TRACK_TOTAL = "track_total" + const val COLUMN_DISC_NUMBER = "disc_number" + const val COLUMN_DISC_TOTAL = "disc_total" + const val COLUMN_DATE = "date" + const val COLUMN_YEAR = "year" + const val COLUMN_DURATION = "duration" + const val COLUMN_BITRATE = "bitrate" + const val COLUMN_SAMPLING_RATE = "sampling_rate" + const val COLUMN_CHANNELS = "channels" + const val COLUMN_ENCODER = "encoder" const val COLUMN_DATE_MODIFIED = "date_modified" + const val COLUMN_SIZE = "size" + const val COLUMN_URI = "uri" + const val COLUMN_REAL_PATH = "real_path" + + fun parse( + path: SimplePath, + parent: MediaTreeFolder, + xfile: DocumentFileX, + id: String, + options: ParseOptions, + ): Extended { + if (options.symphony.settings.useMetaphony.value) { + try { + val file = parseUsingMetaphony(path, parent, xfile, id, options) + if (file != null) { + return file + } + } catch (err: Exception) { + Logger.error("SongFile", "could not parse using metaphony", err) + } + } + return parseUsingMediaMetadataRetriever(path, parent, xfile, id, options) + } + + private fun parseUsingMetaphony( + path: SimplePath, + parent: MediaTreeFolder, + xfile: DocumentFileX, + id: String, + options: ParseOptions, + ): Extended? { + val symphony = options.symphony + val metadata = symphony.applicationContext.contentResolver + .openFileDescriptor(xfile.uri, "r") + ?.use { AudioMetadataParser.parse(xfile.name, it.detachFd()) } + ?: return null + val artwork = metadata.pictures.firstOrNull()?.let { + Extended.Artwork(mimeType = it.mimeType, data = it.data) + } + val file = MediaTreeSongFile( + id = id, + parentId = parent.id, + name = xfile.name, + title = metadata.title ?: path.nameWithoutExtension, + album = metadata.album, + artists = parseMultiValue(metadata.artists, options.artistSeparatorRegex), + composers = parseMultiValue(metadata.composers, options.artistSeparatorRegex), + albumArtists = parseMultiValue(metadata.albumArtists, options.artistSeparatorRegex), + genres = parseMultiValue(metadata.genres, options.genreSeparatorRegex), + trackNumber = metadata.trackNumber, + trackTotal = metadata.trackTotal, + discNumber = metadata.discNumber, + discTotal = metadata.discTotal, + date = metadata.date, + year = metadata.date?.year, + duration = metadata.lengthInSeconds?.let { it * 1000L } ?: 0, + bitrate = metadata.bitrate?.let { it * 1000L }, + samplingRate = metadata.sampleRate?.toLong(), + channels = metadata.channels, + encoder = metadata.encoding, + dateModified = xfile.lastModified, + size = xfile.size, + uri = xfile.uri, + path = path.pathString, + ) + return Extended(file = file, artwork = artwork, lyrics = metadata.lyrics) + } + + fun parseUsingMediaMetadataRetriever( + path: SimplePath, + parent: MediaTreeFolder, + xfile: DocumentFileX, + id: String, + options: ParseOptions, + ): Extended { + val symphony = options.symphony + val retriever = MediaMetadataRetriever() + retriever.setDataSource(symphony.applicationContext, xfile.uri) + val artwork = retriever.embeddedPicture?.let { + Extended.Artwork(mimeType = "_", data = it) + } + val title = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE) + val album = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM) + val artists = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST) + val composers = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_COMPOSER) + val albumArtists = + retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST) + val genres = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_GENRE) + val trackNumber = retriever + .extractMetadata(MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER) + ?.toIntOrNull() + val trackTotal = retriever + .extractMetadata(MediaMetadataRetriever.METADATA_KEY_NUM_TRACKS) + ?.toIntOrNull() + val discNumber = retriever + .extractMetadata(MediaMetadataRetriever.METADATA_KEY_DISC_NUMBER) + ?.toIntOrNull() + val date = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DATE) + val year = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_YEAR) + ?.toIntOrNull() + val duration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION) + ?.toLongOrNull() + val bitrate = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE) + ?.toLongOrNull() + var samplingRate: Long? = null + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + samplingRate = retriever + .extractMetadata(MediaMetadataRetriever.METADATA_KEY_SAMPLERATE) + ?.toLongOrNull() + } + val file = MediaTreeSongFile( + id = id, + parentId = parent.id, + name = xfile.name, + title = title ?: path.nameWithoutExtension, + album = album, + artists = parseMultiValue(artists, options.artistSeparatorRegex), + composers = parseMultiValue(composers, options.artistSeparatorRegex), + albumArtists = parseMultiValue(albumArtists, options.artistSeparatorRegex), + genres = parseMultiValue(genres, options.genreSeparatorRegex), + trackNumber = trackNumber, + trackTotal = trackTotal, + discNumber = discNumber, + discTotal = null, + date = runCatching { LocalDate.parse(date) }.getOrNull(), + year = year, + duration = duration ?: 0, + bitrate = bitrate, + samplingRate = samplingRate, + channels = null, + encoder = null, + dateModified = xfile.lastModified, + size = xfile.size, + uri = xfile.uri, + path = path.pathString, + ) + return Extended(file = file, artwork = artwork, lyrics = null) + } + + private fun makeSeparatorsRegex(separators: Set): Regex { + val partial = separators.joinToString("|") { Pattern.quote(it) } + return Regex("""(?, regex: Regex): Set { + val result = mutableSetOf() + for (x in values) { + for (y in x.trim().split(regex)) { + val trimmed = y.trim() + if (trimmed.isEmpty()) { + continue + } + result.add(trimmed) + } + } + return result + } } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/PlaylistSongMapping.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/PlaylistSongMapping.kt index 5854feea..392a34fe 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/PlaylistSongMapping.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/PlaylistSongMapping.kt @@ -23,6 +23,12 @@ import androidx.room.PrimaryKey childColumns = arrayOf(PlaylistSongMapping.COLUMN_SONG_ID), onDelete = ForeignKey.SET_NULL, ), + ForeignKey( + entity = Song::class, + parentColumns = arrayOf(Song.COLUMN_PATH), + childColumns = arrayOf(PlaylistSongMapping.COLUMN_SONG_PATH), + onDelete = ForeignKey.SET_NULL, + ), ForeignKey( entity = PlaylistSongMapping::class, parentColumns = arrayOf(PlaylistSongMapping.COLUMN_ID), @@ -44,16 +50,19 @@ data class PlaylistSongMapping( val playlistId: String, @ColumnInfo(COLUMN_SONG_ID) val songId: String?, + @ColumnInfo(COLUMN_SONG_PATH) + val songPath: String?, @ColumnInfo(COLUMN_IS_HEAD) val isStart: Boolean, @ColumnInfo(COLUMN_NEXT_ID) val nextId: String?, ) { companion object { - const val TABLE = "playlist_songs" + const val TABLE = "playlist_songs_mapping" const val COLUMN_ID = "id" const val COLUMN_PLAYLIST_ID = "playlist_id" const val COLUMN_SONG_ID = "song_id" + const val COLUMN_SONG_PATH = "song_path" const val COLUMN_IS_HEAD = "is_head" const val COLUMN_NEXT_ID = "next_id" } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Song.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Song.kt index 7104a03f..6d69ba45 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Song.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Song.kt @@ -17,13 +17,16 @@ import java.time.LocalDate Song.TABLE, foreignKeys = [ ForeignKey( - entity = SongFile::class, - parentColumns = arrayOf(SongFile.COLUMN_ID), + entity = MediaTreeSongFile::class, + parentColumns = arrayOf(MediaTreeSongFile.COLUMN_ID), childColumns = arrayOf(Song.COLUMN_ID), onDelete = ForeignKey.CASCADE, ), ], - indices = [Index(Song.COLUMN_TITLE)], + indices = [ + Index(Song.COLUMN_TITLE), + Index(Song.COLUMN_PATH, unique = true), + ], ) data class Song( @PrimaryKey @@ -31,8 +34,6 @@ data class Song( val id: String, @ColumnInfo(COLUMN_TITLE) val title: String, - @ColumnInfo(COLUMN_ALBUM_ID) - val albumId: String?, @ColumnInfo(COLUMN_TRACK_NUMBER) val trackNumber: Int?, @ColumnInfo(COLUMN_TRACK_TOTAL) @@ -59,8 +60,6 @@ data class Song( val dateModified: Long, @ColumnInfo(COLUMN_SIZE) val size: Long, - @ColumnInfo(COLUMN_COVER_FILE) - val coverFile: String?, @ColumnInfo(COLUMN_URI) val uri: Uri, @ColumnInfo(COLUMN_PATH) @@ -106,7 +105,6 @@ data class Song( const val TABLE = "songs" const val COLUMN_ID = "id" const val COLUMN_TITLE = "title" - const val COLUMN_ALBUM_ID = "album" const val COLUMN_TRACK_NUMBER = "track_number" const val COLUMN_TRACK_TOTAL = "track_total" const val COLUMN_DISC_NUMBER = "disc_number" @@ -120,7 +118,6 @@ data class Song( const val COLUMN_ENCODER = "encoder" const val COLUMN_DATE_MODIFIED = "date_modified" const val COLUMN_SIZE = "size" - const val COLUMN_COVER_FILE = "cover_file" const val COLUMN_URI = "uri" const val COLUMN_PATH = "path" } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongFile.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongFile.kt deleted file mode 100644 index bcbccc5a..00000000 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongFile.kt +++ /dev/null @@ -1,267 +0,0 @@ -package io.github.zyrouge.symphony.services.groove.entities - -import android.media.MediaMetadataRetriever -import android.net.Uri -import android.os.Build -import androidx.compose.runtime.Immutable -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.PrimaryKey -import io.github.zyrouge.symphony.Symphony -import io.github.zyrouge.symphony.utils.DocumentFileX -import io.github.zyrouge.symphony.utils.Logger -import io.github.zyrouge.symphony.utils.SimplePath -import me.zyrouge.symphony.metaphony.AudioMetadataParser -import java.time.LocalDate -import java.util.regex.Pattern - -@Immutable -@Entity( - SongFile.TABLE, -) -data class SongFile( - @PrimaryKey - @ColumnInfo(COLUMN_ID) - val id: String, - @ColumnInfo(COLUMN_TITLE) - val title: String, - @ColumnInfo(COLUMN_ALBUM) - val album: String?, - @ColumnInfo(COLUMN_ARTISTS) - val artists: Set, - @ColumnInfo(COLUMN_COMPOSERS) - val composers: Set, - @ColumnInfo(COLUMN_ALBUM_ARTISTS) - val albumArtists: Set, - @ColumnInfo(COLUMN_GENRES) - val genres: Set, - @ColumnInfo(COLUMN_TRACK_NUMBER) - val trackNumber: Int?, - @ColumnInfo(COLUMN_TRACK_TOTAL) - val trackTotal: Int?, - @ColumnInfo(COLUMN_DISC_NUMBER) - val discNumber: Int?, - @ColumnInfo(COLUMN_DISC_TOTAL) - val discTotal: Int?, - @ColumnInfo(COLUMN_DATE) - val date: LocalDate?, - @ColumnInfo(COLUMN_YEAR) - val year: Int?, - @ColumnInfo(COLUMN_DURATION) - val duration: Long, - @ColumnInfo(COLUMN_BITRATE) - val bitrate: Long?, - @ColumnInfo(COLUMN_SAMPLING_RATE) - val samplingRate: Long?, - @ColumnInfo(COLUMN_CHANNELS) - val channels: Int?, - @ColumnInfo(COLUMN_ENCODER) - val encoder: String?, - @ColumnInfo(COLUMN_DATE_MODIFIED) - val dateModified: Long, - @ColumnInfo(COLUMN_SIZE) - val size: Long, - @ColumnInfo(COLUMN_URI) - val uri: Uri, - @ColumnInfo(COLUMN_PATH) - val path: String, -) { - data class ParseOptions( - val symphony: Symphony, - val artistSeparatorRegex: Regex, - val genreSeparatorRegex: Regex, - ) { - companion object { - fun create(symphony: Symphony) = ParseOptions( - symphony = symphony, - artistSeparatorRegex = makeSeparatorsRegex(symphony.settings.artistTagSeparators.value), - genreSeparatorRegex = makeSeparatorsRegex(symphony.settings.genreTagSeparators.value), - ) - } - } - - data class Extended( - val songFile: SongFile, - val artwork: Artwork?, - val lyrics: String?, - ) { - data class Artwork(val mimeType: String, val data: ByteArray) - } - - companion object { - const val TABLE = "song_files" - const val COLUMN_ID = "id" - const val COLUMN_TITLE = "title" - const val COLUMN_ALBUM = "album" - const val COLUMN_ARTISTS = "artists" - const val COLUMN_COMPOSERS = "composers" - const val COLUMN_ALBUM_ARTISTS = "album_artists" - const val COLUMN_GENRES = "genres" - const val COLUMN_TRACK_NUMBER = "track_number" - const val COLUMN_TRACK_TOTAL = "track_total" - const val COLUMN_DISC_NUMBER = "disc_number" - const val COLUMN_DISC_TOTAL = "disc_total" - const val COLUMN_DATE = "date" - const val COLUMN_YEAR = "year" - const val COLUMN_DURATION = "duration" - const val COLUMN_BITRATE = "bitrate" - const val COLUMN_SAMPLING_RATE = "sampling_rate" - const val COLUMN_CHANNELS = "channels" - const val COLUMN_ENCODER = "encoder" - const val COLUMN_DATE_MODIFIED = "date_modified" - const val COLUMN_SIZE = "size" - const val COLUMN_URI = "uri" - const val COLUMN_PATH = "path" - - fun parse( - id: String, - path: SimplePath, - file: DocumentFileX, - options: ParseOptions, - ): Extended { - if (options.symphony.settings.useMetaphony.value) { - try { - val songFile = parseUsingMetaphony(id, path, file, options) - if (songFile != null) { - return songFile - } - } catch (err: Exception) { - Logger.error("SongFile", "could not parse using metaphony", err) - } - } - return parseUsingMediaMetadataRetriever(id, path, file, options) - } - - private fun parseUsingMetaphony( - id: String, - path: SimplePath, - file: DocumentFileX, - options: ParseOptions, - ): Extended? { - val symphony = options.symphony - val metadata = symphony.applicationContext.contentResolver - .openFileDescriptor(file.uri, "r") - ?.use { AudioMetadataParser.parse(file.name, it.detachFd()) } - ?: return null - val artwork = metadata.pictures.firstOrNull()?.let { - Extended.Artwork(mimeType = it.mimeType, data = it.data) - } - val songFile = SongFile( - id = id, - title = metadata.title ?: path.nameWithoutExtension, - album = metadata.album, - artists = parseMultiValue(metadata.artists, options.artistSeparatorRegex), - composers = parseMultiValue(metadata.composers, options.artistSeparatorRegex), - albumArtists = parseMultiValue(metadata.albumArtists, options.artistSeparatorRegex), - genres = parseMultiValue(metadata.genres, options.genreSeparatorRegex), - trackNumber = metadata.trackNumber, - trackTotal = metadata.trackTotal, - discNumber = metadata.discNumber, - discTotal = metadata.discTotal, - date = metadata.date, - year = metadata.date?.year, - duration = metadata.lengthInSeconds?.let { it * 1000L } ?: 0, - bitrate = metadata.bitrate?.let { it * 1000L }, - samplingRate = metadata.sampleRate?.toLong(), - channels = metadata.channels, - encoder = metadata.encoding, - dateModified = file.lastModified, - size = file.size, - uri = file.uri, - path = path.pathString, - ) - return Extended(songFile = songFile, artwork = artwork, lyrics = metadata.lyrics) - } - - fun parseUsingMediaMetadataRetriever( - id: String, - path: SimplePath, - file: DocumentFileX, - options: ParseOptions, - ): Extended { - val symphony = options.symphony - val retriever = MediaMetadataRetriever() - retriever.setDataSource(symphony.applicationContext, file.uri) - val artwork = retriever.embeddedPicture?.let { - Extended.Artwork(mimeType = "_", data = it) - } - val title = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE) - val album = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM) - val artists = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST) - val composers = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_COMPOSER) - val albumArtists = - retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST) - val genres = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_GENRE) - val trackNumber = retriever - .extractMetadata(MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER) - ?.toIntOrNull() - val trackTotal = retriever - .extractMetadata(MediaMetadataRetriever.METADATA_KEY_NUM_TRACKS) - ?.toIntOrNull() - val discNumber = retriever - .extractMetadata(MediaMetadataRetriever.METADATA_KEY_DISC_NUMBER) - ?.toIntOrNull() - val date = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DATE) - val year = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_YEAR) - ?.toIntOrNull() - val duration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION) - ?.toLongOrNull() - val bitrate = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE) - ?.toLongOrNull() - var samplingRate: Long? = null - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - samplingRate = retriever - .extractMetadata(MediaMetadataRetriever.METADATA_KEY_SAMPLERATE) - ?.toLongOrNull() - } - val songFile = SongFile( - id = id, - title = title ?: path.nameWithoutExtension, - album = album, - artists = parseMultiValue(artists, options.artistSeparatorRegex), - composers = parseMultiValue(composers, options.artistSeparatorRegex), - albumArtists = parseMultiValue(albumArtists, options.artistSeparatorRegex), - genres = parseMultiValue(genres, options.genreSeparatorRegex), - trackNumber = trackNumber, - trackTotal = trackTotal, - discNumber = discNumber, - discTotal = null, - date = runCatching { LocalDate.parse(date) }.getOrNull(), - year = year, - duration = duration ?: 0, - bitrate = bitrate, - samplingRate = samplingRate, - channels = null, - encoder = null, - dateModified = file.lastModified, - size = file.size, - uri = file.uri, - path = path.pathString, - ) - return Extended(songFile = songFile, artwork = artwork, lyrics = null) - } - - private fun makeSeparatorsRegex(separators: Set): Regex { - val partial = separators.joinToString("|") { Pattern.quote(it) } - return Regex("""(?, regex: Regex): Set { - val result = mutableSetOf() - for (x in values) { - for (y in x.trim().split(regex)) { - val trimmed = y.trim() - if (trimmed.isEmpty()) { - continue - } - result.add(trimmed) - } - } - return result - } - } -} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongLyrics.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongLyric.kt similarity index 81% rename from app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongLyrics.kt rename to app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongLyric.kt index 846bb1e4..493fc923 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongLyrics.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongLyric.kt @@ -8,17 +8,17 @@ import androidx.room.PrimaryKey @Immutable @Entity( - SongLyrics.TABLE, + SongLyric.TABLE, foreignKeys = [ ForeignKey( entity = Song::class, parentColumns = arrayOf(Song.COLUMN_ID), - childColumns = arrayOf(SongLyrics.COLUMN_SONG_FILE_ID), + childColumns = arrayOf(SongLyric.COLUMN_SONG_FILE_ID), onDelete = ForeignKey.CASCADE, ), ], ) -data class SongLyrics( +data class SongLyric( @PrimaryKey @ColumnInfo(COLUMN_SONG_FILE_ID) val songFileId: String, @@ -26,7 +26,7 @@ data class SongLyrics( val lyrics: String, ) { companion object { - const val TABLE = "playlist_songs_mapping" + const val TABLE = "song_lyrics" const val COLUMN_SONG_FILE_ID = "song_file_id" const val COLUMN_LYRICS = "lyrics" } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/PlaylistRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/PlaylistRepository.kt index 64542fe4..9482329d 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/PlaylistRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/PlaylistRepository.kt @@ -25,7 +25,7 @@ class PlaylistRepository(private val symphony: Symphony) { } private val cache = ConcurrentHashMap() - internal val idGenerator = KeyGenerator.TimeIncremental() + internal val idGenerator = KeyGenerator.TimeCounterRandomMix() private val searcher = FuzzySearcher( options = listOf(FuzzySearchOption({ v -> get(v)?.title?.let { compareString(it) } })) ) diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/KeyGenerator.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/KeyGenerator.kt index d7f0a74f..ec87be62 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/utils/KeyGenerator.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/utils/KeyGenerator.kt @@ -1,27 +1,31 @@ package io.github.zyrouge.symphony.utils +import java.security.SecureRandom + interface KeyGenerator { fun next(): String - class TimeIncremental : KeyGenerator { + class TimeCounterRandomMix : KeyGenerator { private var time = System.currentTimeMillis() private var i = -1 + private val random = SecureRandom() @Synchronized override fun next(): String { + val suffix = random.nextInt(9999) // 256 for the giggles if (i < 256) { i++ - return "$time#$i" + return "$time#$i#$suffix" } val now = System.currentTimeMillis() if (now <= time) { i++ - return "$time#$i" + return "$time#$i#$suffix" } time = now i = 0 - return "$now#0" + return "$now#0#$suffix" } } } From a6fe6ea4796e4b1b2833cf01acc77abf0ce0e8b1 Mon Sep 17 00:00:00 2001 From: Zyrouge Date: Tue, 28 Jan 2025 00:34:50 +0530 Subject: [PATCH 03/15] refactor: add expermental sort queries --- .../1.json | 1150 ++++++++++++++++- .../symphony/services/database/Database.kt | 10 +- .../services/database/PersistentDatabase.kt | 32 +- .../store/AlbumComposerMappingStore.kt | 12 + .../services/database/store/AlbumStore.kt | 32 + .../services/database/store/ArtistStore.kt | 31 + .../store/ComposerSongMappingStore.kt | 12 + .../services/database/store/ComposerStore.kt | 22 + .../services/database/store/GenreStore.kt | 6 + .../store/PlaylistSongMappingStore.kt | 4 + .../services/database/store/PlaylistStore.kt | 6 +- .../services/database/store/SongStore.kt | 81 +- .../symphony/services/groove/Groove.kt | 50 +- .../symphony/services/groove/MediaExposer.kt | 116 +- .../services/groove/entities/Album.kt | 27 +- .../groove/entities/AlbumComposerMapping.kt | 43 + .../services/groove/entities/Artist.kt | 15 + .../services/groove/entities/Composer.kt | 41 + .../groove/entities/ComposerSongMapping.kt | 43 + .../groove/entities/GenreSongMapping.kt | 2 +- .../services/groove/entities/Playlist.kt | 18 +- .../symphony/services/groove/entities/Song.kt | 12 +- .../repositories/AlbumArtistRepository.kt | 66 - .../groove/repositories/AlbumRepository.kt | 115 +- .../symphony/ui/components/AlbumGrid.kt | 21 +- .../github/zyrouge/symphony/ui/view/Home.kt | 6 +- .../zyrouge/symphony/ui/view/home/Albums.kt | 11 +- 27 files changed, 1663 insertions(+), 321 deletions(-) create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumComposerMappingStore.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/database/store/ComposerSongMappingStore.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/database/store/ComposerStore.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/AlbumComposerMapping.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Composer.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/ComposerSongMapping.kt diff --git a/app/room-schemas/io.github.zyrouge.symphony.services.database.PersistentDatabase/1.json b/app/room-schemas/io.github.zyrouge.symphony.services.database.PersistentDatabase/1.json index 438b4b3f..56fea106 100644 --- a/app/room-schemas/io.github.zyrouge.symphony.services.database.PersistentDatabase/1.json +++ b/app/room-schemas/io.github.zyrouge.symphony.services.database.PersistentDatabase/1.json @@ -2,11 +2,646 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "911e8d9ac1e9715396e1465009d548b3", + "identityHash": "4818829a3e04d8013dca6a8e3a0317fd", "entities": [ { - "tableName": "playlists", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `songPaths` TEXT NOT NULL, `title` TEXT, `path` TEXT, PRIMARY KEY(`id`))", + "tableName": "albums", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `start_year` INTEGER, `end_year` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "startYear", + "columnName": "start_year", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "endYear", + "columnName": "end_year", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_albums_name", + "unique": false, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_albums_name` ON `${TABLE_NAME}` (`name`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "album_artists_mapping", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`album_id` TEXT NOT NULL, `artist_id` TEXT NOT NULL, `is_album_artist` INTEGER NOT NULL, PRIMARY KEY(`album_id`, `artist_id`), FOREIGN KEY(`artist_id`) REFERENCES `albums`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artist_id`) REFERENCES `artists`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isAlbumArtist", + "columnName": "is_album_artist", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "album_id", + "artist_id" + ] + }, + "indices": [ + { + "name": "index_album_artists_mapping_album_id", + "unique": false, + "columnNames": [ + "album_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_album_artists_mapping_album_id` ON `${TABLE_NAME}` (`album_id`)" + }, + { + "name": "index_album_artists_mapping_artist_id", + "unique": false, + "columnNames": [ + "artist_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_album_artists_mapping_artist_id` ON `${TABLE_NAME}` (`artist_id`)" + }, + { + "name": "index_album_artists_mapping_is_album_artist", + "unique": false, + "columnNames": [ + "is_album_artist" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_album_artists_mapping_is_album_artist` ON `${TABLE_NAME}` (`is_album_artist`)" + } + ], + "foreignKeys": [ + { + "table": "albums", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "artist_id" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "artists", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "artist_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "album_songs_mapping", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`album_id` TEXT NOT NULL, `song_id` TEXT NOT NULL, PRIMARY KEY(`album_id`, `song_id`), FOREIGN KEY(`album_id`) REFERENCES `albums`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`song_id`) REFERENCES `songs`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "songId", + "columnName": "song_id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "album_id", + "song_id" + ] + }, + "indices": [ + { + "name": "index_album_songs_mapping_album_id", + "unique": false, + "columnNames": [ + "album_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_album_songs_mapping_album_id` ON `${TABLE_NAME}` (`album_id`)" + }, + { + "name": "index_album_songs_mapping_song_id", + "unique": false, + "columnNames": [ + "song_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_album_songs_mapping_song_id` ON `${TABLE_NAME}` (`song_id`)" + } + ], + "foreignKeys": [ + { + "table": "albums", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "album_id" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "songs", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "song_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "artists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_artists_name", + "unique": false, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_artists_name` ON `${TABLE_NAME}` (`name`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "artist_songs_mapping", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`artist_id` TEXT NOT NULL, `song_id` TEXT NOT NULL, PRIMARY KEY(`artist_id`, `song_id`), FOREIGN KEY(`artist_id`) REFERENCES `album_artists_mapping`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`song_id`) REFERENCES `songs`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "songId", + "columnName": "song_id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "artist_id", + "song_id" + ] + }, + "indices": [ + { + "name": "index_artist_songs_mapping_artist_id", + "unique": false, + "columnNames": [ + "artist_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_artist_songs_mapping_artist_id` ON `${TABLE_NAME}` (`artist_id`)" + }, + { + "name": "index_artist_songs_mapping_song_id", + "unique": false, + "columnNames": [ + "song_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_artist_songs_mapping_song_id` ON `${TABLE_NAME}` (`song_id`)" + } + ], + "foreignKeys": [ + { + "table": "album_artists_mapping", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "artist_id" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "songs", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "song_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "artwork_indices", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`song_id` TEXT NOT NULL, `file` TEXT, PRIMARY KEY(`song_id`), FOREIGN KEY(`song_id`) REFERENCES `songs`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "songId", + "columnName": "song_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "file", + "columnName": "file", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "song_id" + ] + }, + "indices": [ + { + "name": "index_artwork_indices_file", + "unique": false, + "columnNames": [ + "file" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_artwork_indices_file` ON `${TABLE_NAME}` (`file`)" + } + ], + "foreignKeys": [ + { + "table": "songs", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "song_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "genres", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_genres_name", + "unique": false, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_genres_name` ON `${TABLE_NAME}` (`name`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "genre_songs_mapping", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`genre_id` TEXT NOT NULL, `song_id` TEXT NOT NULL, PRIMARY KEY(`genre_id`, `song_id`), FOREIGN KEY(`genre_id`) REFERENCES `album_artists_mapping`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`song_id`) REFERENCES `songs`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "genreId", + "columnName": "genre_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "songId", + "columnName": "song_id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "genre_id", + "song_id" + ] + }, + "indices": [ + { + "name": "index_genre_songs_mapping_genre_id", + "unique": false, + "columnNames": [ + "genre_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_genre_songs_mapping_genre_id` ON `${TABLE_NAME}` (`genre_id`)" + }, + { + "name": "index_genre_songs_mapping_song_id", + "unique": false, + "columnNames": [ + "song_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_genre_songs_mapping_song_id` ON `${TABLE_NAME}` (`song_id`)" + } + ], + "foreignKeys": [ + { + "table": "album_artists_mapping", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "genre_id" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "songs", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "song_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "media_tree_folders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `parent_id` TEXT, `name` TEXT NOT NULL, `is_head` INTEGER NOT NULL, `date_modified` INTEGER NOT NULL, `uri` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`parent_id`) REFERENCES `media_tree_folders`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isHead", + "columnName": "is_head", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dateModified", + "columnName": "date_modified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_media_tree_folders_parent_id", + "unique": false, + "columnNames": [ + "parent_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_media_tree_folders_parent_id` ON `${TABLE_NAME}` (`parent_id`)" + }, + { + "name": "index_media_tree_folders_name", + "unique": false, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_media_tree_folders_name` ON `${TABLE_NAME}` (`name`)" + }, + { + "name": "index_media_tree_folders_is_head", + "unique": false, + "columnNames": [ + "is_head" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_media_tree_folders_is_head` ON `${TABLE_NAME}` (`is_head`)" + } + ], + "foreignKeys": [ + { + "table": "media_tree_folders", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "parent_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "media_tree_lyric_files", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `parent_id` TEXT NOT NULL, `name` TEXT NOT NULL, `date_modified` INTEGER NOT NULL, `uri` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`parent_id`) REFERENCES `media_tree_folders`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`name`) REFERENCES `playlists`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dateModified", + "columnName": "date_modified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_media_tree_lyric_files_parent_id", + "unique": false, + "columnNames": [ + "parent_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_media_tree_lyric_files_parent_id` ON `${TABLE_NAME}` (`parent_id`)" + }, + { + "name": "index_media_tree_lyric_files_name", + "unique": false, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_media_tree_lyric_files_name` ON `${TABLE_NAME}` (`name`)" + } + ], + "foreignKeys": [ + { + "table": "media_tree_folders", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "parent_id" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "playlists", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "name" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "media_tree_song_files", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `parent_id` TEXT NOT NULL, `name` TEXT NOT NULL, `title` TEXT NOT NULL, `album` TEXT, `artists` TEXT NOT NULL, `composers` TEXT NOT NULL, `album_artists` TEXT NOT NULL, `genres` TEXT NOT NULL, `track_number` INTEGER, `track_total` INTEGER, `disc_number` INTEGER, `disc_total` INTEGER, `date` TEXT, `year` INTEGER, `duration` INTEGER NOT NULL, `bitrate` INTEGER, `sampling_rate` INTEGER, `channels` INTEGER, `encoder` TEXT, `date_modified` INTEGER NOT NULL, `size` INTEGER NOT NULL, `uri` TEXT NOT NULL, `real_path` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`parent_id`) REFERENCES `media_tree_folders`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "id", @@ -14,6 +649,18 @@ "affinity": "TEXT", "notNull": true }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, { "fieldPath": "title", "columnName": "title", @@ -21,8 +668,179 @@ "notNull": true }, { - "fieldPath": "songPaths", - "columnName": "songPaths", + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artists", + "columnName": "artists", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "composers", + "columnName": "composers", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "albumArtists", + "columnName": "album_artists", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "genres", + "columnName": "genres", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "trackNumber", + "columnName": "track_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "trackTotal", + "columnName": "track_total", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discTotal", + "columnName": "disc_total", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bitrate", + "columnName": "bitrate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "samplingRate", + "columnName": "sampling_rate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "channels", + "columnName": "channels", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "encoder", + "columnName": "encoder", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "dateModified", + "columnName": "date_modified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "path", + "columnName": "real_path", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_media_tree_song_files_parent_id", + "unique": false, + "columnNames": [ + "parent_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_media_tree_song_files_parent_id` ON `${TABLE_NAME}` (`parent_id`)" + }, + { + "name": "index_media_tree_song_files_name", + "unique": false, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_media_tree_song_files_name` ON `${TABLE_NAME}` (`name`)" + } + ], + "foreignKeys": [ + { + "table": "media_tree_folders", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "parent_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "playlists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `title` TEXT, `path` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", "affinity": "TEXT", "notNull": true }, @@ -45,14 +863,332 @@ "id" ] }, - "indices": [], + "indices": [ + { + "name": "index_playlists_title", + "unique": false, + "columnNames": [ + "title" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_playlists_title` ON `${TABLE_NAME}` (`title`)" + } + ], "foreignKeys": [] + }, + { + "tableName": "playlist_songs_mapping", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlist_id` TEXT NOT NULL, `song_id` TEXT, `song_path` TEXT, `is_head` INTEGER NOT NULL, `next_id` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`song_id`) REFERENCES `songs`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`song_path`) REFERENCES `songs`(`path`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`next_id`) REFERENCES `playlist_songs_mapping`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "playlistId", + "columnName": "playlist_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "songId", + "columnName": "song_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "songPath", + "columnName": "song_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isStart", + "columnName": "is_head", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nextId", + "columnName": "next_id", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_playlist_songs_mapping_playlist_id", + "unique": false, + "columnNames": [ + "playlist_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_songs_mapping_playlist_id` ON `${TABLE_NAME}` (`playlist_id`)" + }, + { + "name": "index_playlist_songs_mapping_is_head", + "unique": false, + "columnNames": [ + "is_head" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_songs_mapping_is_head` ON `${TABLE_NAME}` (`is_head`)" + }, + { + "name": "index_playlist_songs_mapping_next_id", + "unique": false, + "columnNames": [ + "next_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_songs_mapping_next_id` ON `${TABLE_NAME}` (`next_id`)" + } + ], + "foreignKeys": [ + { + "table": "playlists", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "playlist_id" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "songs", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "song_id" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "songs", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "song_path" + ], + "referencedColumns": [ + "path" + ] + }, + { + "table": "playlist_songs_mapping", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "next_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "songs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `track_number` INTEGER, `track_total` INTEGER, `disc_number` INTEGER, `disc_total` INTEGER, `date` TEXT, `year` INTEGER, `duration` INTEGER NOT NULL, `bitrate` INTEGER, `sampling_rate` INTEGER, `channels` INTEGER, `encoder` TEXT, `date_modified` INTEGER NOT NULL, `size` INTEGER NOT NULL, `uri` TEXT NOT NULL, `path` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `media_tree_song_files`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "trackNumber", + "columnName": "track_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "trackTotal", + "columnName": "track_total", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discTotal", + "columnName": "disc_total", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bitrate", + "columnName": "bitrate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "samplingRate", + "columnName": "sampling_rate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "channels", + "columnName": "channels", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "encoder", + "columnName": "encoder", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "dateModified", + "columnName": "date_modified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_songs_title", + "unique": false, + "columnNames": [ + "title" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_songs_title` ON `${TABLE_NAME}` (`title`)" + }, + { + "name": "index_songs_path", + "unique": true, + "columnNames": [ + "path" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_songs_path` ON `${TABLE_NAME}` (`path`)" + } + ], + "foreignKeys": [ + { + "table": "media_tree_song_files", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "song_lyrics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`song_file_id` TEXT NOT NULL, `lyrics` TEXT NOT NULL, PRIMARY KEY(`song_file_id`), FOREIGN KEY(`song_file_id`) REFERENCES `songs`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "songFileId", + "columnName": "song_file_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lyrics", + "columnName": "lyrics", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "song_file_id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "songs", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "song_file_id" + ], + "referencedColumns": [ + "id" + ] + } + ] } ], "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '911e8d9ac1e9715396e1465009d548b3')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4818829a3e04d8013dca6a8e3a0317fd')" ] } } \ No newline at end of file diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/Database.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/Database.kt index 9592fbf4..ee28f597 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/Database.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/Database.kt @@ -8,28 +8,26 @@ class Database(symphony: Symphony) { val persistent = PersistentDatabase.create(symphony) val albumsIdGenerator = KeyGenerator.TimeCounterRandomMix() - val albumArtistMappingIdGenerator = KeyGenerator.TimeCounterRandomMix() - val albumSongMappingIdGenerator = KeyGenerator.TimeCounterRandomMix() val artistsIdGenerator = KeyGenerator.TimeCounterRandomMix() - val artistSongMappingIdGenerator = KeyGenerator.TimeCounterRandomMix() val artworksIdGenerator = KeyGenerator.TimeCounterRandomMix() - val artworkIndicesIdGenerator = KeyGenerator.TimeCounterRandomMix() + val composersIdGenerator = KeyGenerator.TimeCounterRandomMix() val genresIdGenerator = KeyGenerator.TimeCounterRandomMix() - val genreSongMappingIdGenerator = KeyGenerator.TimeCounterRandomMix() val mediaTreeFoldersIdGenerator = KeyGenerator.TimeCounterRandomMix() val mediaTreeSongFilesIdGenerator = KeyGenerator.TimeCounterRandomMix() val mediaTreeLyricFilesIdGenerator = KeyGenerator.TimeCounterRandomMix() val playlistsIdGenerator = KeyGenerator.TimeCounterRandomMix() val playlistSongMappingIdGenerator = KeyGenerator.TimeCounterRandomMix() - val songLyricsIdGenerator = KeyGenerator.TimeCounterRandomMix() val albums get() = persistent.albums() val albumArtistMapping get() = persistent.albumArtistMapping() + val albumComposerMapping get() = persistent.albumComposerMapping() val albumSongMapping get() = persistent.albumSongMapping() val artists get() = persistent.artists() val artistSongMapping get() = persistent.artistSongMapping() val artworks = ArtworkStore(symphony) val artworkIndices get() = persistent.artworkIndices() + val composers get() = persistent.composers() + val composerSongMapping get() = persistent.composerSongMapping() val genres get() = persistent.genre() val genreSongMapping get() = persistent.genreSongMapping() val mediaTreeFolders get() = persistent.mediaTreeFolders() diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/PersistentDatabase.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/PersistentDatabase.kt index 60142025..8753323d 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/PersistentDatabase.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/PersistentDatabase.kt @@ -11,6 +11,8 @@ import io.github.zyrouge.symphony.services.database.store.AlbumStore import io.github.zyrouge.symphony.services.database.store.ArtistSongMappingStore import io.github.zyrouge.symphony.services.database.store.ArtistStore import io.github.zyrouge.symphony.services.database.store.ArtworkIndexStore +import io.github.zyrouge.symphony.services.database.store.ComposerSongMappingStore +import io.github.zyrouge.symphony.services.database.store.ComposerStore import io.github.zyrouge.symphony.services.database.store.GenreSongMappingStore import io.github.zyrouge.symphony.services.database.store.GenreStore import io.github.zyrouge.symphony.services.database.store.MediaTreeFolderStore @@ -22,12 +24,18 @@ import io.github.zyrouge.symphony.services.database.store.SongLyricStore import io.github.zyrouge.symphony.services.database.store.SongStore import io.github.zyrouge.symphony.services.groove.entities.Album import io.github.zyrouge.symphony.services.groove.entities.AlbumArtistMapping +import io.github.zyrouge.symphony.services.groove.entities.AlbumComposerMapping import io.github.zyrouge.symphony.services.groove.entities.AlbumSongMapping import io.github.zyrouge.symphony.services.groove.entities.Artist import io.github.zyrouge.symphony.services.groove.entities.ArtistSongMapping import io.github.zyrouge.symphony.services.groove.entities.ArtworkIndex +import io.github.zyrouge.symphony.services.groove.entities.Composer +import io.github.zyrouge.symphony.services.groove.entities.ComposerSongMapping import io.github.zyrouge.symphony.services.groove.entities.Genre import io.github.zyrouge.symphony.services.groove.entities.GenreSongMapping +import io.github.zyrouge.symphony.services.groove.entities.MediaTreeFolder +import io.github.zyrouge.symphony.services.groove.entities.MediaTreeLyricFile +import io.github.zyrouge.symphony.services.groove.entities.MediaTreeSongFile import io.github.zyrouge.symphony.services.groove.entities.Playlist import io.github.zyrouge.symphony.services.groove.entities.PlaylistSongMapping import io.github.zyrouge.symphony.services.groove.entities.Song @@ -37,31 +45,37 @@ import io.github.zyrouge.symphony.utils.RoomConvertors @Database( version = 1, entities = [ + Album::class, AlbumArtistMapping::class, + AlbumComposerMapping::class, AlbumSongMapping::class, - Album::class, - ArtistSongMapping::class, Artist::class, + ArtistSongMapping::class, ArtworkIndex::class, - GenreSongMapping::class, + Composer::class, + ComposerSongMapping::class, Genre::class, - MediaTreeFolderStore::class, - MediaTreeLyricFileStore::class, - MediaTreeSongFileStore::class, - PlaylistSongMapping::class, + GenreSongMapping::class, + MediaTreeFolder::class, + MediaTreeLyricFile::class, + MediaTreeSongFile::class, Playlist::class, - SongLyric::class, + PlaylistSongMapping::class, Song::class, + SongLyric::class, ], ) @TypeConverters(RoomConvertors::class) abstract class PersistentDatabase : RoomDatabase() { abstract fun albumArtistMapping(): AlbumArtistMappingStore + abstract fun albumComposerMapping(): AlbumComposerMapping abstract fun albumSongMapping(): AlbumSongMappingStore abstract fun albums(): AlbumStore abstract fun artistSongMapping(): ArtistSongMappingStore abstract fun artists(): ArtistStore abstract fun artworkIndices(): ArtworkIndexStore + abstract fun composerSongMapping(): ComposerSongMappingStore + abstract fun composers(): ComposerStore abstract fun genreSongMapping(): GenreSongMappingStore abstract fun genre(): GenreStore abstract fun mediaTreeFolders(): MediaTreeFolderStore @@ -73,7 +87,7 @@ abstract class PersistentDatabase : RoomDatabase() { abstract fun songs(): SongStore companion object { - const val DATABASE_NAME = "symphony_persistent" + const val DATABASE_NAME = "persistent" fun create(symphony: Symphony) = Room .databaseBuilder( diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumComposerMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumComposerMappingStore.kt new file mode 100644 index 00000000..7f91d25d --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumComposerMappingStore.kt @@ -0,0 +1,12 @@ +package io.github.zyrouge.symphony.services.database.store + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import io.github.zyrouge.symphony.services.groove.entities.AlbumComposerMapping + +@Dao +interface AlbumComposerMappingStore { + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsert(vararg entities: AlbumComposerMapping) +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumStore.kt index 692ccc2d..16200123 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumStore.kt @@ -3,8 +3,15 @@ package io.github.zyrouge.symphony.services.database.store import androidx.room.Dao import androidx.room.Insert import androidx.room.Query +import androidx.room.RawQuery import androidx.room.Update +import androidx.sqlite.db.SimpleSQLiteQuery +import androidx.sqlite.db.SupportSQLiteQuery import io.github.zyrouge.symphony.services.groove.entities.Album +import io.github.zyrouge.symphony.services.groove.entities.AlbumArtistMapping +import io.github.zyrouge.symphony.services.groove.entities.AlbumSongMapping +import io.github.zyrouge.symphony.services.groove.repositories.AlbumRepository +import kotlinx.coroutines.flow.Flow @Dao interface AlbumStore { @@ -16,4 +23,29 @@ interface AlbumStore { @Query("SELECT * FROM ${Album.TABLE} WHERE ${Album.COLUMN_NAME} = :name LIMIT 1") fun findByName(name: String): Album? + + @RawQuery(observedEntities = [Album::class, AlbumArtistMapping::class, AlbumSongMapping::class]) + fun valuesAsFlowRaw(query: SupportSQLiteQuery): Flow> +} + +fun AlbumStore.valuesAsFlow( + sortBy: AlbumRepository.SortBy, + sortReverse: Boolean, +): Flow> { + val orderBy = when (sortBy) { + AlbumRepository.SortBy.CUSTOM -> "${Album.TABLE}.${Album.COLUMN_ID}" + AlbumRepository.SortBy.ALBUM_NAME -> "${Album.TABLE}.${Album.COLUMN_NAME}" + AlbumRepository.SortBy.YEAR -> "${Album.TABLE}.${Album.COLUMN_START_YEAR}" + AlbumRepository.SortBy.TRACKS_COUNT -> Album.AlongAttributes.EMBEDDED_TRACKS_COUNT + AlbumRepository.SortBy.ARTISTS_COUNT -> Album.AlongAttributes.EMBEDDED_ARTISTS_COUNT + } + val orderDirection = if (sortReverse) "DESC" else "ASC" + val query = "SELECT ${Album.TABLE}.*, " + + "COUNT(${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_SONG_ID}) as ${Album.AlongAttributes.EMBEDDED_TRACKS_COUNT}, " + + "COUNT(${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ARTIST_ID}) as ${Album.AlongAttributes.EMBEDDED_ARTISTS_COUNT} " + + "FROM ${Album.TABLE} " + + "LEFT JOIN ${AlbumSongMapping.TABLE} ON ${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_ALBUM_ID} = ${Album.TABLE}.${Album.COLUMN_ID} " + + "LEFT JOIN ${AlbumArtistMapping.TABLE} ON ${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID} = ${Album.TABLE}.${Album.COLUMN_ID} " + + "ORDER BY $orderBy $orderDirection" + return valuesAsFlowRaw(SimpleSQLiteQuery(query)) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistStore.kt index ce2f2e40..e9623573 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistStore.kt @@ -4,8 +4,15 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.MapColumn import androidx.room.Query +import androidx.room.RawQuery import androidx.room.Update +import androidx.sqlite.db.SimpleSQLiteQuery +import androidx.sqlite.db.SupportSQLiteQuery +import io.github.zyrouge.symphony.services.groove.entities.AlbumArtistMapping import io.github.zyrouge.symphony.services.groove.entities.Artist +import io.github.zyrouge.symphony.services.groove.entities.ArtistSongMapping +import io.github.zyrouge.symphony.services.groove.repositories.ArtistRepository +import kotlinx.coroutines.flow.Flow @Dao interface ArtistStore { @@ -15,8 +22,32 @@ interface ArtistStore { @Update suspend fun update(vararg entities: Artist): Int + @RawQuery(observedEntities = [Artist::class, ArtistSongMapping::class, AlbumArtistMapping::class]) + fun valuesAsFlowRaw(query: SupportSQLiteQuery): Flow> + @Query("SELECT ${Artist.COLUMN_ID}, ${Artist.COLUMN_NAME} FROM ${Artist.TABLE} WHERE ${Artist.COLUMN_NAME} in (:names)") fun entriesByNameNameIdMapped(names: Collection): Map< @MapColumn(Artist.COLUMN_NAME) String, @MapColumn(Artist.COLUMN_ID) String> } + +fun ArtistStore.valuesAsFlow( + sortBy: ArtistRepository.SortBy, + sortReverse: Boolean, +): Flow> { + val orderBy = when (sortBy) { + ArtistRepository.SortBy.CUSTOM -> "${Artist.TABLE}.${Artist.COLUMN_ID}" + ArtistRepository.SortBy.ARTIST_NAME -> "${Artist.TABLE}.${Artist.COLUMN_NAME}" + ArtistRepository.SortBy.TRACKS_COUNT -> Artist.AlongAttributes.EMBEDDED_TRACKS_COUNT + ArtistRepository.SortBy.ALBUMS_COUNT -> Artist.AlongAttributes.EMBEDDED_ALBUMS_COUNT + } + val orderDirection = if (sortReverse) "DESC" else "ASC" + val query = "SELECT ${Artist.TABLE}.*, " + + "COUNT(${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_SONG_ID}) as ${Artist.AlongAttributes.EMBEDDED_TRACKS_COUNT}, " + + "COUNT(${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID}) as ${Artist.AlongAttributes.EMBEDDED_ALBUMS_COUNT} " + + "FROM ${Artist.TABLE} " + + "LEFT JOIN ${ArtistSongMapping.TABLE} ON ${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_ARTIST_ID} = ${Artist.TABLE}.${Artist.COLUMN_ID} " + + "LEFT JOIN ${AlbumArtistMapping.TABLE} ON ${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ARTIST_ID} = ${Artist.TABLE}.${Artist.COLUMN_ID}" + + "ORDER BY $orderBy $orderDirection" + return valuesAsFlowRaw(SimpleSQLiteQuery(query)) +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ComposerSongMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ComposerSongMappingStore.kt new file mode 100644 index 00000000..31309535 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ComposerSongMappingStore.kt @@ -0,0 +1,12 @@ +package io.github.zyrouge.symphony.services.database.store + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import io.github.zyrouge.symphony.services.groove.entities.ComposerSongMapping + +@Dao +interface ComposerSongMappingStore { + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsert(vararg entities: ComposerSongMapping) +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ComposerStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ComposerStore.kt new file mode 100644 index 00000000..4700ad7d --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ComposerStore.kt @@ -0,0 +1,22 @@ +package io.github.zyrouge.symphony.services.database.store + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.MapColumn +import androidx.room.Query +import androidx.room.Update +import io.github.zyrouge.symphony.services.groove.entities.Composer + +@Dao +interface ComposerStore { + @Insert + suspend fun insert(vararg entities: Composer): List + + @Update + suspend fun update(vararg entities: Composer): Int + + @Query("SELECT ${Composer.COLUMN_ID}, ${Composer.COLUMN_NAME} FROM ${Composer.TABLE} WHERE ${Composer.COLUMN_NAME} in (:names)") + fun entriesByNameNameIdMapped(names: Collection): Map< + @MapColumn(Composer.COLUMN_NAME) String, + @MapColumn(Composer.COLUMN_ID) String> +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreStore.kt index ae642287..6e91259a 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreStore.kt @@ -2,6 +2,7 @@ package io.github.zyrouge.symphony.services.database.store import androidx.room.Dao import androidx.room.Insert +import androidx.room.MapColumn import androidx.room.Query import io.github.zyrouge.symphony.services.groove.entities.Genre @@ -12,4 +13,9 @@ interface GenreStore { @Query("SELECT * FROM ${Genre.TABLE} WHERE ${Genre.COLUMN_NAME} = :name LIMIT 1") fun findByName(name: String): Genre? + + @Query("SELECT ${Genre.COLUMN_ID}, ${Genre.COLUMN_NAME} FROM ${Genre.TABLE} WHERE ${Genre.COLUMN_NAME} in (:names)") + fun entriesByNameNameIdMapped(names: Collection): Map< + @MapColumn(Genre.COLUMN_NAME) String, + @MapColumn(Genre.COLUMN_ID) String> } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistSongMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistSongMappingStore.kt index 39d4dead..7d01fbbf 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistSongMappingStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistSongMappingStore.kt @@ -2,10 +2,14 @@ package io.github.zyrouge.symphony.services.database.store import androidx.room.Dao import androidx.room.Insert +import androidx.room.Query import io.github.zyrouge.symphony.services.groove.entities.PlaylistSongMapping @Dao interface PlaylistSongMappingStore { @Insert suspend fun insert(vararg entities: PlaylistSongMapping) + + @Query("DELETE FROM ${PlaylistSongMapping.TABLE} WHERE ${PlaylistSongMapping.COLUMN_PLAYLIST_ID} IN (:ids)") + suspend fun deletePlaylistIds(ids: Collection) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistStore.kt index 524752ca..307bfbb1 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistStore.kt @@ -18,9 +18,9 @@ interface PlaylistStore { @Query("DELETE FROM ${Playlist.TABLE} WHERE ${Playlist.COLUMN_ID} = :id") suspend fun delete(id: String): Int - @Query("SELECT * FROM ${Playlist.TABLE}") - suspend fun values(): List + @Query("SELECT * FROM ${Playlist.TABLE} WHERE ${Playlist.COLUMN_URI} != NULL") + fun valuesLocalOnly(): List @Query("SELECT * FROM ${Playlist.TABLE}") - suspend fun valuesAsFlow(): Flow> + fun valuesAsFlow(): Flow> } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongStore.kt index 0e430bf2..ba3b8219 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongStore.kt @@ -3,12 +3,24 @@ package io.github.zyrouge.symphony.services.database.store import androidx.room.Dao import androidx.room.Insert import androidx.room.Query +import androidx.room.RawQuery import androidx.room.Update +import androidx.sqlite.db.SimpleSQLiteQuery +import androidx.sqlite.db.SupportSQLiteQuery +import io.github.zyrouge.symphony.services.groove.entities.Album +import io.github.zyrouge.symphony.services.groove.entities.AlbumArtistMapping +import io.github.zyrouge.symphony.services.groove.entities.AlbumSongMapping +import io.github.zyrouge.symphony.services.groove.entities.Artist +import io.github.zyrouge.symphony.services.groove.entities.ArtistSongMapping +import io.github.zyrouge.symphony.services.groove.entities.Composer +import io.github.zyrouge.symphony.services.groove.entities.ComposerSongMapping import io.github.zyrouge.symphony.services.groove.entities.Song +import io.github.zyrouge.symphony.services.groove.repositories.SongRepository +import kotlinx.coroutines.flow.Flow @Dao interface SongStore { - @Insert() + @Insert suspend fun insert(vararg entities: Song): List @Update @@ -25,4 +37,71 @@ interface SongStore { @Query("SELECT ${Song.COLUMN_ID} FROM ${Song.TABLE}") fun ids(): List + + @RawQuery( + observedEntities = [ + Song::class, + Artist::class, + ArtistSongMapping::class, + Album::class, + AlbumSongMapping::class, + AlbumArtistMapping::class, + Composer::class, + ComposerSongMapping::class, + ] + ) + fun valuesAsFlowRaw(query: SupportSQLiteQuery): Flow> +} + +fun SongStore.valuesAsFlow( + sortBy: SongRepository.SortBy, + sortReverse: Boolean, +): Flow> { + val aliasAlbumArtist = "albumArtist" + val embeddedArtistName = "artistName" + val embeddedAlbumName = "albumName" + val embeddedAlbumArtistName = "albumArtistName" + val embeddedComposerName = "composerName" + val orderBy = when (sortBy) { + SongRepository.SortBy.CUSTOM -> "${Song.TABLE}.${Song.COLUMN_ID}" + SongRepository.SortBy.TITLE -> "${Song.TABLE}.${Song.COLUMN_ID}" + SongRepository.SortBy.ARTIST -> embeddedArtistName + SongRepository.SortBy.ALBUM -> embeddedAlbumName + SongRepository.SortBy.DURATION -> "${Song.TABLE}.${Song.COLUMN_ID}" + SongRepository.SortBy.DATE_MODIFIED -> "${Song.TABLE}.${Song.COLUMN_ID}" + SongRepository.SortBy.COMPOSER -> embeddedComposerName + SongRepository.SortBy.ALBUM_ARTIST -> embeddedAlbumArtistName + SongRepository.SortBy.YEAR -> "${Song.TABLE}.${Song.COLUMN_YEAR}" + SongRepository.SortBy.FILENAME -> "${Song.TABLE}.${Song.COLUMN_FILENAME}" + SongRepository.SortBy.TRACK_NUMBER -> "${Song.TABLE}.${Song.COLUMN_TRACK_NUMBER}" + } + val orderDirection = if (sortReverse) "DESC" else "ASC" + val artistQuery = "SELECT" + + "TOP 1 ${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_ARTIST_ID} " + + "FROM ${ArtistSongMapping.TABLE} " + + "WHERE ${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_SONG_ID} = ${Song.COLUMN_ID}" + val albumQuery = "SELECT" + + "TOP 1 ${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_ALBUM_ID} " + + "FROM ${AlbumSongMapping.TABLE} " + + "WHERE ${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_SONG_ID} = ${Song.COLUMN_ID}" + val albumArtistQuery = "SELECT " + + "TOP 1 ${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ARTIST_ID} " + + "FROM ${AlbumArtistMapping.TABLE} " + + "WHERE ${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID} = ${Album.COLUMN_ID}" + val composerQuery = "SELECT " + + "TOP 1 ${ComposerSongMapping.TABLE}.${ComposerSongMapping.COLUMN_COMPOSER_ID} " + + "FROM ${ComposerSongMapping.TABLE} " + + "WHERE ${ComposerSongMapping.TABLE}.${ComposerSongMapping.COLUMN_SONG_ID} = ${Song.COLUMN_ID}" + val query = "SELECT ${Song.TABLE}.*, " + + "${Artist.TABLE}.${Artist.COLUMN_NAME} as $embeddedArtistName, " + + "${Album.TABLE}.${Album.COLUMN_NAME} as $embeddedAlbumName, " + + "$aliasAlbumArtist.${Artist.COLUMN_NAME} as $embeddedAlbumArtistName, " + + "${Composer.TABLE}.${Composer.COLUMN_NAME} as $embeddedComposerName " + + "FROM ${Song.TABLE} " + + "LEFT JOIN ${Artist.TABLE} ON ${Artist.TABLE}.${Artist.COLUMN_ID} = ($artistQuery)" + + "LEFT JOIN ${Album.TABLE} ON ${Album.TABLE}.${Album.COLUMN_ID} = ($albumQuery)" + + "LEFT JOIN ${Artist.TABLE} $aliasAlbumArtist ON ${Artist.TABLE}.${Artist.COLUMN_ID} = ($albumArtistQuery)" + + "LEFT JOIN ${Composer.TABLE} ON ${Composer.TABLE}.${Composer.COLUMN_ID} = ($composerQuery)" + + "ORDER BY $orderBy $orderDirection" + return valuesAsFlowRaw(SimpleSQLiteQuery(query)) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/Groove.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/Groove.kt index a8835f10..5bf5f313 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/Groove.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/Groove.kt @@ -10,8 +10,6 @@ import io.github.zyrouge.symphony.services.groove.repositories.SongRepository import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll import kotlinx.coroutines.launch class Groove(private val symphony: Symphony) : Symphony.Hooks { @@ -25,7 +23,7 @@ class Groove(private val symphony: Symphony) : Symphony.Hooks { } val coroutineScope = CoroutineScope(Dispatchers.Default) - var readyDeferred = CompletableDeferred() + val readyDeferred = CompletableDeferred() val exposer = MediaExposer(symphony) val song = SongRepository(symphony) @@ -35,55 +33,15 @@ class Groove(private val symphony: Symphony) : Symphony.Hooks { val genre = GenreRepository(symphony) val playlist = PlaylistRepository(symphony) - private suspend fun fetch() { + fun refetch() { coroutineScope.launch { - awaitAll( - async { exposer.fetch() }, - async { playlist.fetch() }, - ) - }.join() - } - - private suspend fun reset() { - coroutineScope.launch { - awaitAll( - async { exposer.reset() }, - async { albumArtist.reset() }, - async { album.reset() }, - async { artist.reset() }, - async { genre.reset() }, - async { playlist.reset() }, - async { song.reset() }, - ) - }.join() - } - - private suspend fun clearCache() { - symphony.database.songCache.clear() - symphony.database.artworkCache.clear() - symphony.database.lyricsCache.clear() - } - - data class FetchOptions( - val resetInMemoryCache: Boolean = false, - val resetPersistentCache: Boolean = false, - ) - - fun fetch(options: FetchOptions) { - coroutineScope.launch { - if (options.resetInMemoryCache) { - reset() - } - if (options.resetPersistentCache) { - clearCache() - } - fetch() + exposer.fetch() } } override fun onSymphonyReady() { coroutineScope.launch { - fetch() + exposer.fetch() readyDeferred.complete(true) } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/MediaExposer.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/MediaExposer.kt index ab775a1b..c53746bf 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/MediaExposer.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/MediaExposer.kt @@ -9,11 +9,15 @@ import io.github.zyrouge.symphony.services.groove.entities.AlbumSongMapping import io.github.zyrouge.symphony.services.groove.entities.Artist import io.github.zyrouge.symphony.services.groove.entities.ArtistSongMapping import io.github.zyrouge.symphony.services.groove.entities.ArtworkIndex +import io.github.zyrouge.symphony.services.groove.entities.Composer +import io.github.zyrouge.symphony.services.groove.entities.ComposerSongMapping import io.github.zyrouge.symphony.services.groove.entities.Genre import io.github.zyrouge.symphony.services.groove.entities.GenreSongMapping import io.github.zyrouge.symphony.services.groove.entities.MediaTreeFolder import io.github.zyrouge.symphony.services.groove.entities.MediaTreeLyricFile import io.github.zyrouge.symphony.services.groove.entities.MediaTreeSongFile +import io.github.zyrouge.symphony.services.groove.entities.Playlist +import io.github.zyrouge.symphony.services.groove.entities.PlaylistSongMapping import io.github.zyrouge.symphony.services.groove.entities.Song import io.github.zyrouge.symphony.services.groove.entities.SongLyric import io.github.zyrouge.symphony.utils.ActivityUtils @@ -24,7 +28,6 @@ import io.github.zyrouge.symphony.utils.Logger import io.github.zyrouge.symphony.utils.SimplePath import io.github.zyrouge.symphony.utils.concurrentSetOf import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope @@ -39,18 +42,25 @@ import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds class MediaExposer(private val symphony: Symphony) { - private val _isUpdating = MutableStateFlow(false) - val isUpdating = _isUpdating.asStateFlow() + private val _isUpdating = MutableStateFlow(false) + val isUpdating get() = _isUpdating.asStateFlow() - private fun emitUpdate(value: Boolean) = _isUpdating.update { value } - - @OptIn(ExperimentalCoroutinesApi::class) suspend fun fetch() { - emitUpdate(true) + _isUpdating.update { true } + coroutineScope { + awaitAll( + async { scanMediaTree() }, + async { scanPlaylists() }, + ) + } + _isUpdating.update { false } + } + + suspend fun scanMediaTree() { try { val context = symphony.applicationContext val folderUris = symphony.settings.mediaFolders.value - val scanner = Scanner.create(symphony) + val scanner = MediaTreeScanner.create(symphony) folderUris.map { x -> ActivityUtils.makePersistableReadableUri(context, x) DocumentFileX.fromTreeUri(context, x)?.let { @@ -64,11 +74,44 @@ class MediaExposer(private val symphony: Symphony) { } catch (err: Exception) { Logger.error("MediaExposer", "fetch failed", err) } - emitUpdate(false) - emitFinish() } - private data class Scanner( + suspend fun scanPlaylists() { + try { + val playlists = symphony.database.playlists.valuesLocalOnly() + val playlistsToBeUpdated = mutableListOf() + val playlistIdsToBeDeletedInMapping = mutableListOf() + val playlistSongMappingToBeInserted = mutableListOf() + for (exPlaylist in playlists) { + val playlistId = exPlaylist.id + val uri = exPlaylist.uri!! + playlistIdsToBeDeletedInMapping.add(playlistId) + val extended = Playlist.parse(symphony, playlistId, uri) + playlistsToBeUpdated.add(extended.playlist) + var nextPlaylistSongMapping: PlaylistSongMapping? = null + for (i in (extended.songPaths.size - 1) downTo 0) { + val x = extended.songPaths[i] + val playlistSongMapping = PlaylistSongMapping( + id = symphony.database.playlistSongMappingIdGenerator.next(), + playlistId = playlistId, + songId = null, + songPath = x, + isStart = i == 0, + nextId = nextPlaylistSongMapping?.id, + ) + playlistSongMappingToBeInserted.add(playlistSongMapping) + nextPlaylistSongMapping = playlistSongMapping + } + } + symphony.database.playlists.update(*playlistsToBeUpdated.toTypedArray()) + symphony.database.playlistSongMapping.deletePlaylistIds(playlistIdsToBeDeletedInMapping) + symphony.database.playlistSongMapping.insert(*playlistSongMappingToBeInserted.toTypedArray()) + } catch (err: Exception) { + Logger.error("MediaExposer", "playlist fetch failed", err) + } + } + + private data class MediaTreeScanner( val symphony: Symphony, val rootFolder: MediaTreeFolder, val folderStaleIds: ConcurrentSet, @@ -183,14 +226,15 @@ class MediaExposer(private val symphony: Symphony) { if (extension == null) { return@let null } + val artworkId = symphony.database.artworksIdGenerator.next() val quality = symphony.settings.artworkQuality.value if (quality.maxSide == null && extension != "_") { - val name = "$id.$extension" + val name = "$artworkId.$extension" symphony.database.artworks.get(name).writeBytes(it.data) return@let name } val bitmap = BitmapFactory.decodeByteArray(it.data, 0, it.data.size) - val name = "$id.jpg" + val name = "$artworkId.jpg" FileOutputStream(symphony.database.artworks.get(name)).use { writer -> ImagePreserver .resize(bitmap, quality) @@ -233,6 +277,7 @@ class MediaExposer(private val symphony: Symphony) { encoder = file.encoder, dateModified = file.dateModified, size = file.size, + filename = file.name, uri = file.uri, path = file.path, ) @@ -253,6 +298,7 @@ class MediaExposer(private val symphony: Symphony) { encoder = file.encoder, dateModified = file.dateModified, size = file.size, + filename = file.name, uri = file.uri, path = file.path, ) @@ -326,22 +372,36 @@ class MediaExposer(private val symphony: Symphony) { symphony.database.artists.insert(*artistsToBeInserted.toTypedArray()) symphony.database.artistSongMapping.upsert(*artistSongMappingsToBeUpserted.toTypedArray()) symphony.database.albumArtistMapping.upsert(*albumArtistMappingsToBeUpserted.values.toTypedArray()) + val exComposerIds = + symphony.database.composers.entriesByNameNameIdMapped(file.composers) + val composersToBeInserted = mutableListOf() + val composerSongMappingsToBeUpserted = mutableListOf() + for (composerName in file.composers) { + val exComposerId = exComposerIds[composerName] + val composerId = exComposerId ?: symphony.database.composersIdGenerator.next() + if (exComposerId == null) { + val composer = Composer(id = composerId, name = composerName) + composersToBeInserted.add(composer) + } + val composerSongMapping = ComposerSongMapping(composerId = composerId, songId = id) + composerSongMappingsToBeUpserted.add(composerSongMapping) + } + symphony.database.composers.insert(*composersToBeInserted.toTypedArray()) + symphony.database.composerSongMapping.upsert(*composerSongMappingsToBeUpserted.toTypedArray()) + val exGenreIds = symphony.database.genres.entriesByNameNameIdMapped(file.genres) + val genresToBeInserted = mutableListOf() val genreSongMappingsToBeUpserted = mutableListOf() for (genreName in file.genres) { - val exGenre = symphony.database.genres.findByName(genreName) - val genre = when { - exGenre != null -> exGenre - else -> Genre( - id = symphony.database.genresIdGenerator.next(), - name = genreName, - ) - } - if (exGenre == null) { - symphony.database.genres.insert(genre) + val exGenreId = exGenreIds[genreName] + val genreId = exGenreId ?: symphony.database.genresIdGenerator.next() + if (exGenreId == null) { + val genre = Genre(id = genreId, name = genreName) + genresToBeInserted.add(genre) } - val genreSongMapping = GenreSongMapping(genreId = genre.id, songId = id) + val genreSongMapping = GenreSongMapping(genreId = genreId, songId = id) genreSongMappingsToBeUpserted.add(genreSongMapping) } + symphony.database.genres.insert(*genresToBeInserted.toTypedArray()) symphony.database.genreSongMapping.upsert(*genreSongMappingsToBeUpserted.toTypedArray()) } @@ -385,7 +445,7 @@ class MediaExposer(private val symphony: Symphony) { } companion object { - suspend fun create(symphony: Symphony): Scanner { + suspend fun create(symphony: Symphony): MediaTreeScanner { val filter = MediaFilter( symphony.settings.songsFilterPattern.value, symphony.settings.blacklistFolders.value.toSortedSet(), @@ -396,7 +456,7 @@ class MediaExposer(private val symphony: Symphony) { val songFileIds = symphony.database.mediaTreeSongFiles.ids(rootFolder.id) val lyricFileIds = symphony.database.mediaTreeLyricFiles.ids(rootFolder.id) val songIds = symphony.database.songs.ids() - return Scanner( + return MediaTreeScanner( symphony = symphony, rootFolder = rootFolder, folderStaleIds = concurrentSetOf(folderIds), @@ -428,10 +488,6 @@ class MediaExposer(private val symphony: Symphony) { } } - private fun emitFinish() { - symphony.groove.playlist.onScanFinish() - } - private class MediaFilter( pattern: String?, private val blacklisted: Set, diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Album.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Album.kt index def3579d..8e8cc586 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Album.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Album.kt @@ -2,6 +2,7 @@ package io.github.zyrouge.symphony.services.groove.entities import androidx.compose.runtime.Immutable import androidx.room.ColumnInfo +import androidx.room.Embedded import androidx.room.Entity import androidx.room.Index import androidx.room.PrimaryKey @@ -23,15 +24,29 @@ data class Album( @ColumnInfo(COLUMN_END_YEAR) val endYear: Int?, ) { + data class AlongAttributes( + @Embedded + val album: Album, + @Embedded + val tracksCount: Int, + @Embedded + val artistsCount: Int, + ) { + companion object { + const val EMBEDDED_TRACKS_COUNT = "tracksCount" + const val EMBEDDED_ARTISTS_COUNT = "artistsCount" + } + } + fun createArtworkImageRequest(symphony: Symphony) = symphony.groove.album.createArtworkImageRequest(id) - fun getSongIds(symphony: Symphony) = symphony.groove.album.getSongIds(id) - fun getSortedSongIds(symphony: Symphony) = symphony.groove.song.sort( - getSongIds(symphony), - symphony.settings.lastUsedAlbumSongsSortBy.value, - symphony.settings.lastUsedAlbumSongsSortReverse.value, - ) + // fun getSongIds(symphony: Symphony) = symphony.groove.album.getSongIds(id) +// fun getSortedSongIds(symphony: Symphony) = symphony.groove.song.sort( +// getSongIds(symphony), +// symphony.settings.lastUsedAlbumSongsSortBy.value, +// symphony.settings.lastUsedAlbumSongsSortReverse.value, +// ) companion object { const val TABLE = "albums" diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/AlbumComposerMapping.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/AlbumComposerMapping.kt new file mode 100644 index 00000000..221bb5b4 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/AlbumComposerMapping.kt @@ -0,0 +1,43 @@ +package io.github.zyrouge.symphony.services.groove.entities + +import androidx.compose.runtime.Immutable +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index + +@Immutable +@Entity( + AlbumComposerMapping.TABLE, + primaryKeys = [AlbumComposerMapping.COLUMN_ALBUM_ID, AlbumComposerMapping.COLUMN_COMPOSER_ID], + foreignKeys = [ + ForeignKey( + entity = Album::class, + parentColumns = arrayOf(Album.COLUMN_ID), + childColumns = arrayOf(AlbumComposerMapping.COLUMN_ALBUM_ID), + onDelete = ForeignKey.CASCADE, + ), + ForeignKey( + entity = Artist::class, + parentColumns = arrayOf(Composer.COLUMN_ID), + childColumns = arrayOf(AlbumComposerMapping.COLUMN_COMPOSER_ID), + onDelete = ForeignKey.CASCADE, + ), + ], + indices = [ + Index(AlbumComposerMapping.COLUMN_ALBUM_ID), + Index(AlbumComposerMapping.COLUMN_COMPOSER_ID), + ], +) +data class AlbumComposerMapping( + @ColumnInfo(COLUMN_ALBUM_ID) + val albumId: String, + @ColumnInfo(COLUMN_COMPOSER_ID) + val composerId: String, +) { + companion object { + const val TABLE = "album_composer_mapping" + const val COLUMN_ALBUM_ID = "album_id" + const val COLUMN_COMPOSER_ID = "composer_id" + } +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Artist.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Artist.kt index fc878638..f50f2f2d 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Artist.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Artist.kt @@ -2,6 +2,7 @@ package io.github.zyrouge.symphony.services.groove.entities import androidx.compose.runtime.Immutable import androidx.room.ColumnInfo +import androidx.room.Embedded import androidx.room.Entity import androidx.room.Index import androidx.room.PrimaryKey @@ -19,6 +20,20 @@ data class Artist( @ColumnInfo(COLUMN_NAME) val name: String, ) { + data class AlongAttributes( + @Embedded + val artist: Artist, + @Embedded + val tracksCount: Int, + @Embedded + val albumsCount: Int, + ) { + companion object { + const val EMBEDDED_TRACKS_COUNT = "tracksCount" + const val EMBEDDED_ALBUMS_COUNT = "albumsCount" + } + } + fun createArtworkImageRequest(symphony: Symphony) = symphony.groove.artist.createArtworkImageRequest(name) diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Composer.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Composer.kt new file mode 100644 index 00000000..9ebbb56f --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Composer.kt @@ -0,0 +1,41 @@ +package io.github.zyrouge.symphony.services.groove.entities + +import androidx.compose.runtime.Immutable +import androidx.room.ColumnInfo +import androidx.room.Embedded +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey + +@Immutable +@Entity( + Composer.TABLE, + indices = [Index(Composer.COLUMN_NAME)], +) +data class Composer( + @PrimaryKey + @ColumnInfo(COLUMN_ID) + val id: String, + @ColumnInfo(COLUMN_NAME) + val name: String, +) { + data class AlongAttributes( + @Embedded + val composer: Composer, + @Embedded + val tracksCount: Int, + @Embedded + val albumsCount: Int, + ) { + companion object { + const val EMBEDDED_TRACKS_COUNT = "tracksCount" + const val EMBEDDED_ALBUMS_COUNT = "albumsCount" + } + } + + companion object { + const val TABLE = "composers" + const val COLUMN_ID = "id" + const val COLUMN_NAME = "name" + } +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/ComposerSongMapping.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/ComposerSongMapping.kt new file mode 100644 index 00000000..ae091cfb --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/ComposerSongMapping.kt @@ -0,0 +1,43 @@ +package io.github.zyrouge.symphony.services.groove.entities + +import androidx.compose.runtime.Immutable +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index + +@Immutable +@Entity( + ComposerSongMapping.TABLE, + primaryKeys = [ComposerSongMapping.COLUMN_COMPOSER_ID, ComposerSongMapping.COLUMN_SONG_ID], + foreignKeys = [ + ForeignKey( + entity = Composer::class, + parentColumns = arrayOf(Composer.COLUMN_ID), + childColumns = arrayOf(ComposerSongMapping.COLUMN_COMPOSER_ID), + onDelete = ForeignKey.CASCADE, + ), + ForeignKey( + entity = Song::class, + parentColumns = arrayOf(Song.COLUMN_ID), + childColumns = arrayOf(ComposerSongMapping.COLUMN_SONG_ID), + onDelete = ForeignKey.CASCADE, + ), + ], + indices = [ + Index(ComposerSongMapping.COLUMN_COMPOSER_ID), + Index(ComposerSongMapping.COLUMN_SONG_ID), + ], +) +data class ComposerSongMapping( + @ColumnInfo(COLUMN_COMPOSER_ID) + val composerId: String, + @ColumnInfo(COLUMN_SONG_ID) + val songId: String, +) { + companion object { + const val TABLE = "composer_songs_mapping" + const val COLUMN_COMPOSER_ID = "composer_id" + const val COLUMN_SONG_ID = "song_id" + } +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/GenreSongMapping.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/GenreSongMapping.kt index 4ca02b55..2489a1d0 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/GenreSongMapping.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/GenreSongMapping.kt @@ -12,7 +12,7 @@ import androidx.room.Index primaryKeys = [GenreSongMapping.COLUMN_GENRE_ID, GenreSongMapping.COLUMN_SONG_ID], foreignKeys = [ ForeignKey( - entity = AlbumArtistMapping::class, + entity = Genre::class, parentColumns = arrayOf(Genre.COLUMN_ID), childColumns = arrayOf(GenreSongMapping.COLUMN_GENRE_ID), onDelete = ForeignKey.CASCADE, diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Playlist.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Playlist.kt index 4e05595a..155bf2b6 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Playlist.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Playlist.kt @@ -24,13 +24,13 @@ data class Playlist( val id: String, @ColumnInfo(COLUMN_TITLE) val title: String, - val songPaths: List, @ColumnInfo(COLUMN_URI) val uri: Uri?, @ColumnInfo(COLUMN_PATH) val path: String?, ) { - val numberOfTracks: Int get() = songPaths.size + data class Extended(val playlist: Playlist, val songPaths: List) + val isLocal get() = uri != null val isNotLocal get() = uri == null @@ -58,14 +58,6 @@ data class Playlist( symphony.settings.lastUsedPlaylistSongsSortReverse.value, ) - fun withTitle(title: String) = Playlist( - id = id, - title = title, - songPaths = songPaths, - uri = uri, - path = path, - ) - companion object { const val TABLE = "playlists" const val COLUMN_ID = "id" @@ -75,7 +67,7 @@ data class Playlist( private const val PRIMARY_STORAGE = "primary:" - fun parse(symphony: Symphony, playlistId: String?, uri: Uri): Playlist { + fun parse(symphony: Symphony, playlistId: String?, uri: Uri): Extended { val file = DocumentFileX.fromSingleUri(symphony.applicationContext, uri)!! val content = symphony.applicationContext.contentResolver.openInputStream(uri) ?.use { String(it.readBytes()) } ?: "" @@ -85,13 +77,13 @@ data class Playlist( .toList() val id = playlistId ?: symphony.groove.playlist.idGenerator.next() val path = DocumentFileX.getParentPathOfSingleUri(file.uri) ?: file.name - return Playlist( + val playlist = Playlist( id = id, title = Path(path).nameWithoutExtension, - songPaths = songPaths, uri = uri, path = path, ) + return Extended(playlist = playlist, songPaths = songPaths) } } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Song.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Song.kt index 6d69ba45..35b01071 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Song.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Song.kt @@ -3,12 +3,12 @@ package io.github.zyrouge.symphony.services.groove.entities import android.net.Uri import androidx.compose.runtime.Immutable import androidx.room.ColumnInfo +import androidx.room.Embedded import androidx.room.Entity import androidx.room.ForeignKey import androidx.room.Index import androidx.room.PrimaryKey import io.github.zyrouge.symphony.Symphony -import io.github.zyrouge.symphony.utils.SimplePath import java.math.RoundingMode import java.time.LocalDate @@ -60,11 +60,18 @@ data class Song( val dateModified: Long, @ColumnInfo(COLUMN_SIZE) val size: Long, + @ColumnInfo(COLUMN_FILENAME) + val filename: String, @ColumnInfo(COLUMN_URI) val uri: Uri, @ColumnInfo(COLUMN_PATH) val path: String, ) { + data class AlongAttribute( + @Embedded + val song: Song, + ) + val bitrateK: Long? get() = bitrate?.let { it / 1000 } val samplingRateK: Float? get() = samplingRate?.let { @@ -74,8 +81,6 @@ data class Song( .toFloat() } - val filename get() = SimplePath(path).name - fun createArtworkImageRequest(symphony: Symphony) = symphony.groove.song.createArtworkImageRequest(id) @@ -118,6 +123,7 @@ data class Song( const val COLUMN_ENCODER = "encoder" const val COLUMN_DATE_MODIFIED = "date_modified" const val COLUMN_SIZE = "size" + const val COLUMN_FILENAME = "filename" const val COLUMN_URI = "uri" const val COLUMN_PATH = "path" } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumArtistRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumArtistRepository.kt index 40762029..0dda2ad4 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumArtistRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumArtistRepository.kt @@ -1,19 +1,9 @@ package io.github.zyrouge.symphony.services.groove.repositories import io.github.zyrouge.symphony.Symphony -import io.github.zyrouge.symphony.services.groove.AlbumArtist -import io.github.zyrouge.symphony.services.groove.Song import io.github.zyrouge.symphony.ui.helpers.Assets import io.github.zyrouge.symphony.ui.helpers.createHandyImageRequest -import io.github.zyrouge.symphony.utils.ConcurrentSet -import io.github.zyrouge.symphony.utils.FuzzySearchOption -import io.github.zyrouge.symphony.utils.FuzzySearcher -import io.github.zyrouge.symphony.utils.concurrentSetOf import io.github.zyrouge.symphony.utils.withCase -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import java.util.concurrent.ConcurrentHashMap class AlbumArtistRepository(private val symphony: Symphony) { enum class SortBy { @@ -23,62 +13,6 @@ class AlbumArtistRepository(private val symphony: Symphony) { ALBUMS_COUNT, } - private val cache = ConcurrentHashMap() - private val songIdsCache = ConcurrentHashMap>() - private val albumIdsCache = ConcurrentHashMap>() - private val searcher = FuzzySearcher( - options = listOf(FuzzySearchOption({ v -> get(v)?.name?.let { compareString(it) } })) - ) - - val isUpdating get() = symphony.groove.exposer.isUpdating - private val _all = MutableStateFlow>(emptyList()) - val all = _all.asStateFlow() - private val _count = MutableStateFlow(0) - val count = _count.asStateFlow() - - private fun emitCount() = _count.update { - cache.size - } - - internal fun onSong(song: Song) { - song.albumArtists.forEach { albumArtist -> - songIdsCache.compute(albumArtist) { _, value -> - value?.apply { add(song.id) } ?: concurrentSetOf(song.id) - } - var nNumberOfAlbums = 0 - symphony.groove.album.getIdFromSong(song)?.let { albumId -> - albumIdsCache.compute(albumArtist) { _, value -> - nNumberOfAlbums = (value?.size ?: 0) + 1 - value?.apply { add(albumId) } ?: concurrentSetOf(albumId) - } - } - cache.compute(albumArtist) { _, value -> - value?.apply { - numberOfAlbums = nNumberOfAlbums - numberOfTracks++ - } ?: run { - _all.update { - it + albumArtist - } - emitCount() - AlbumArtist( - name = albumArtist, - numberOfAlbums = 1, - numberOfTracks = 1, - ) - } - } - } - } - - fun reset() { - cache.clear() - _all.update { - emptyList() - } - emitCount() - } - fun getArtworkUri(albumArtistName: String) = songIdsCache[albumArtistName]?.firstOrNull() ?.let { symphony.groove.song.getArtworkUri(it) } ?: symphony.groove.song.getDefaultArtworkUri() diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumRepository.kt index 8dd4bf21..49dc4e4e 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumRepository.kt @@ -1,105 +1,20 @@ package io.github.zyrouge.symphony.services.groove.repositories import io.github.zyrouge.symphony.Symphony -import io.github.zyrouge.symphony.services.groove.Album -import io.github.zyrouge.symphony.services.groove.Song +import io.github.zyrouge.symphony.services.database.store.AlbumStore import io.github.zyrouge.symphony.ui.helpers.Assets import io.github.zyrouge.symphony.ui.helpers.createHandyImageRequest -import io.github.zyrouge.symphony.utils.ConcurrentSet -import io.github.zyrouge.symphony.utils.FuzzySearchOption -import io.github.zyrouge.symphony.utils.FuzzySearcher -import io.github.zyrouge.symphony.utils.concurrentSetOf -import io.github.zyrouge.symphony.utils.joinToStringIfNotEmpty import io.github.zyrouge.symphony.utils.withCase -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import java.util.concurrent.ConcurrentHashMap -import kotlin.math.max -import kotlin.math.min -import kotlin.time.Duration.Companion.milliseconds class AlbumRepository(private val symphony: Symphony) { enum class SortBy { CUSTOM, ALBUM_NAME, - ARTIST_NAME, TRACKS_COUNT, + ARTISTS_COUNT, YEAR, } - private val cache = ConcurrentHashMap() - private val songIdsCache = ConcurrentHashMap>() - private val searcher = FuzzySearcher( - options = listOf( - FuzzySearchOption({ v -> get(v)?.name?.let { compareString(it) } }, 3), - FuzzySearchOption({ v -> get(v)?.artists?.let { compareCollection(it) } }) - ) - ) - - val isUpdating get() = symphony.groove.exposer.isUpdating - private val _all = MutableStateFlow>(emptyList()) - val all = _all.asStateFlow() - private val _count = MutableStateFlow(0) - val count = _count.asStateFlow() - - private fun emitCount() = _count.update { - cache.size - } - - internal fun onSong(song: Song) { - val albumId = getIdFromSong(song) ?: return - songIdsCache.compute(albumId) { _, value -> - value?.apply { add(song.id) } ?: concurrentSetOf(song.id) - } - cache.compute(albumId) { _, value -> - value?.apply { - artists.addAll(song.artists) - song.year?.let { - startYear = startYear?.let { old -> min(old, it) } ?: it - endYear = endYear?.let { old -> max(old, it) } ?: it - } - numberOfTracks++ - duration += song.duration.milliseconds - } ?: run { - _all.update { - it + albumId - } - emitCount() - Album( - id = albumId, - name = song.album!!, - artists = mutableSetOf().apply { - // ensure that album artists are first - addAll(song.albumArtists) - addAll(song.artists) - }, - startYear = song.year, - endYear = song.year, - numberOfTracks = 1, - duration = song.duration.milliseconds, - ) - } - } - } - - fun reset() { - cache.clear() - songIdsCache.clear() - _all.update { - emptyList() - } - emitCount() - } - - fun getIdFromSong(song: Song): String? { - if (song.album == null) { - return null - } - val artists = song.albumArtists.sorted().joinToString("-") - return "${song.album}-${artists}" - } - fun getArtworkUri(albumId: String) = songIdsCache[albumId]?.firstOrNull() ?.let { symphony.groove.song.getArtworkUri(it) } ?: symphony.groove.song.getDefaultArtworkUri() @@ -113,26 +28,18 @@ class AlbumRepository(private val symphony: Symphony) { fun search(albumIds: List, terms: String, limit: Int = 7) = searcher .search(terms, albumIds, maxLength = limit) - fun sort(albumIds: List, by: SortBy, reverse: Boolean): List { + fun sort( + albums: List, + by: SortBy, + reverse: Boolean, + ): List { val sensitive = symphony.settings.caseSensitiveSorting.value val sorted = when (by) { - SortBy.CUSTOM -> albumIds - SortBy.ALBUM_NAME -> albumIds.sortedBy { get(it)?.name?.withCase(sensitive) } - SortBy.ARTIST_NAME -> albumIds.sortedBy { - get(it)?.artists?.joinToStringIfNotEmpty(sensitive) - } - - SortBy.TRACKS_COUNT -> albumIds.sortedBy { get(it)?.numberOfTracks } - SortBy.YEAR -> albumIds.sortedBy { get(it)?.startYear } + SortBy.CUSTOM -> albums + SortBy.ALBUM_NAME -> albums.sortedBy { it.album.name.withCase(sensitive) } + SortBy.TRACKS_COUNT -> albums.sortedBy { it.tracksCount } + SortBy.YEAR -> albums.sortedBy { it.album.startYear } } return if (reverse) sorted.reversed() else sorted } - - fun count() = cache.size - fun ids() = cache.keys.toList() - fun values() = cache.values.toList() - - fun get(albumId: String) = cache[albumId] - fun get(albumIds: List) = albumIds.mapNotNull { get(it) }.toList() - fun getSongIds(albumId: String) = songIdsCache[albumId]?.toList() ?: emptyList() } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumGrid.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumGrid.kt index 9957c7f8..ebc38ff4 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumGrid.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumGrid.kt @@ -14,21 +14,18 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import io.github.zyrouge.symphony.services.groove.Groove +import io.github.zyrouge.symphony.services.groove.entities.Album import io.github.zyrouge.symphony.services.groove.repositories.AlbumRepository import io.github.zyrouge.symphony.ui.helpers.ViewContext @OptIn(ExperimentalMaterial3Api::class) @Composable -fun AlbumGrid( - context: ViewContext, - albumIds: List, - albumsCount: Int? = null, -) { +fun AlbumGrid(context: ViewContext, albums: List) { val sortBy by context.symphony.settings.lastUsedAlbumsSortBy.flow.collectAsState() val sortReverse by context.symphony.settings.lastUsedAlbumsSortReverse.flow.collectAsState() - val sortedAlbumIds by remember(albumIds, sortBy, sortReverse) { + val sortedAlbumIds by remember(albums, sortBy, sortReverse) { derivedStateOf { - context.symphony.groove.album.sort(albumIds, sortBy, sortReverse) + context.symphony.groove.album.sort(albums, sortBy, sortReverse) } } val horizontalGridColumns by context.symphony.settings.lastUsedAlbumsHorizontalGridColumns.flow.collectAsState() @@ -56,7 +53,7 @@ fun AlbumGrid( context.symphony.settings.lastUsedAlbumsSortBy.setValue(it) }, label = { - Text(context.symphony.t.XAlbums((albumsCount ?: albumIds.size).toString())) + Text(context.symphony.t.XAlbums((albums.size).toString())) }, onShowModifyLayout = { showModifyLayoutSheet = true @@ -65,7 +62,7 @@ fun AlbumGrid( }, content = { when { - albumIds.isEmpty() -> IconTextBody( + albums.isEmpty() -> IconTextBody( icon = { modifier -> Icon( Icons.Filled.Album, @@ -81,10 +78,8 @@ fun AlbumGrid( sortedAlbumIds, key = { i, x -> "$i-$x" }, contentType = { _, _ -> Groove.Kind.ALBUM } - ) { _, albumId -> - context.symphony.groove.album.get(albumId)?.let { album -> - AlbumTile(context, album) - } + ) { _, album -> + AlbumTile(context, album) } } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Home.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Home.kt index 6d2fd1d2..7f07de37 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Home.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Home.kt @@ -62,7 +62,6 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -169,7 +168,6 @@ object HomeViewRoute @OptIn(ExperimentalMaterial3Api::class) @Composable fun HomeView(context: ViewContext) { - val coroutineScope = rememberCoroutineScope() val readIntroductoryMessage by context.symphony.settings.readIntroductoryMessage.flow.collectAsState() val tabs by context.symphony.settings.homeTabs.flow.collectAsState() val labelVisibility by context.symphony.settings.homePageBottomBarLabelVisibility.flow.collectAsState() @@ -228,9 +226,7 @@ fun HomeView(context: ViewContext) { onClick = { showOptionsDropdown = false context.symphony.radio.stop() - context.symphony.groove.fetch( - Groove.FetchOptions(resetInMemoryCache = true), - ) + context.symphony.groove.refetch() } ) DropdownMenuItem( diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Albums.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Albums.kt index 0f4156b9..a4f3c0c5 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Albums.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Albums.kt @@ -9,15 +9,10 @@ import io.github.zyrouge.symphony.ui.helpers.ViewContext @Composable fun AlbumsView(context: ViewContext) { - val isUpdating by context.symphony.groove.album.isUpdating.collectAsState() - val albumIds by context.symphony.groove.album.all.collectAsState() - val albumsCount by context.symphony.groove.album.count.collectAsState() + val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsState() + val albums by context.symphony.database.albums.valuesAsFlow().collectAsState(emptyList()) LoaderScaffold(context, isLoading = isUpdating) { - AlbumGrid( - context, - albumIds = albumIds, - albumsCount = albumsCount, - ) + AlbumGrid(context, albums = albums) } } From a0fd8989a5b256a3b5a13cb9ac29c9284b240a15 Mon Sep 17 00:00:00 2001 From: Zyrouge Date: Wed, 29 Jan 2025 00:24:16 +0530 Subject: [PATCH 04/15] refactor: partial ui changes to use newer methods --- .../services/database/PersistentDatabase.kt | 4 +- .../database/store/AlbumArtistMappingStore.kt | 32 +++ .../database/store/AlbumSongMappingStore.kt | 13 + .../services/database/store/AlbumStore.kt | 16 +- .../database/store/ArtworkIndexStore.kt | 8 +- .../services/database/store/ComposerStore.kt | 31 +++ .../services/database/store/GenreStore.kt | 29 ++- .../services/database/store/PlaylistStore.kt | 27 +- .../services/database/store/SongStore.kt | 34 +-- .../symphony/services/groove/MediaExposer.kt | 7 +- .../services/groove/entities/Album.kt | 11 - .../services/groove/entities/Genre.kt | 18 +- .../services/groove/entities/Playlist.kt | 43 +--- .../{ArtworkIndex.kt => SongArtworkIndex.kt} | 10 +- .../repositories/AlbumArtistRepository.kt | 40 +-- .../groove/repositories/AlbumRepository.kt | 42 +--- .../groove/repositories/ArtistRepository.kt | 104 +------- .../groove/repositories/ComposerRepository.kt | 16 ++ .../groove/repositories/GenreRepository.kt | 80 +----- .../groove/repositories/PlaylistRepository.kt | 233 +----------------- .../groove/repositories/SongRepository.kt | 130 +--------- .../symphony/ui/components/AlbumArtistGrid.kt | 28 +-- .../symphony/ui/components/AlbumGrid.kt | 19 +- .../symphony/ui/components/ArtistGrid.kt | 25 +- .../symphony/ui/components/GenreGrid.kt | 152 ++++++------ .../symphony/ui/components/PlaylistGrid.kt | 29 +-- .../symphony/ui/components/SongList.kt | 64 ++--- .../github/zyrouge/symphony/ui/view/Album.kt | 52 ++-- .../symphony/ui/view/home/AlbumArtists.kt | 17 +- .../zyrouge/symphony/ui/view/home/Albums.kt | 16 +- .../zyrouge/symphony/ui/view/home/Artists.kt | 17 +- .../zyrouge/symphony/ui/view/home/Genres.kt | 13 +- .../symphony/ui/view/home/Playlists.kt | 21 +- .../zyrouge/symphony/ui/view/home/Songs.kt | 17 +- 34 files changed, 487 insertions(+), 911 deletions(-) rename app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/{ArtworkIndex.kt => SongArtworkIndex.kt} (75%) create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/ComposerRepository.kt diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/PersistentDatabase.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/PersistentDatabase.kt index 8753323d..525bdf0d 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/PersistentDatabase.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/PersistentDatabase.kt @@ -28,7 +28,6 @@ import io.github.zyrouge.symphony.services.groove.entities.AlbumComposerMapping import io.github.zyrouge.symphony.services.groove.entities.AlbumSongMapping import io.github.zyrouge.symphony.services.groove.entities.Artist import io.github.zyrouge.symphony.services.groove.entities.ArtistSongMapping -import io.github.zyrouge.symphony.services.groove.entities.ArtworkIndex import io.github.zyrouge.symphony.services.groove.entities.Composer import io.github.zyrouge.symphony.services.groove.entities.ComposerSongMapping import io.github.zyrouge.symphony.services.groove.entities.Genre @@ -39,6 +38,7 @@ import io.github.zyrouge.symphony.services.groove.entities.MediaTreeSongFile import io.github.zyrouge.symphony.services.groove.entities.Playlist import io.github.zyrouge.symphony.services.groove.entities.PlaylistSongMapping import io.github.zyrouge.symphony.services.groove.entities.Song +import io.github.zyrouge.symphony.services.groove.entities.SongArtworkIndex import io.github.zyrouge.symphony.services.groove.entities.SongLyric import io.github.zyrouge.symphony.utils.RoomConvertors @@ -51,7 +51,7 @@ import io.github.zyrouge.symphony.utils.RoomConvertors AlbumSongMapping::class, Artist::class, ArtistSongMapping::class, - ArtworkIndex::class, + SongArtworkIndex::class, Composer::class, ComposerSongMapping::class, Genre::class, diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumArtistMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumArtistMappingStore.kt index d1d340ef..c37e5a6d 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumArtistMappingStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumArtistMappingStore.kt @@ -3,10 +3,42 @@ package io.github.zyrouge.symphony.services.database.store import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy +import androidx.room.RawQuery +import androidx.sqlite.db.SimpleSQLiteQuery +import androidx.sqlite.db.SupportSQLiteQuery import io.github.zyrouge.symphony.services.groove.entities.AlbumArtistMapping +import io.github.zyrouge.symphony.services.groove.entities.Artist +import io.github.zyrouge.symphony.services.groove.entities.ArtistSongMapping +import io.github.zyrouge.symphony.services.groove.repositories.AlbumArtistRepository +import kotlinx.coroutines.flow.Flow @Dao interface AlbumArtistMappingStore { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun upsert(vararg entities: AlbumArtistMapping) + + @RawQuery(observedEntities = [AlbumArtistMapping::class, Artist::class, ArtistSongMapping::class]) + fun valuesAsFlowRaw(query: SupportSQLiteQuery): Flow> +} + +fun AlbumArtistMappingStore.valuesAsFlow( + sortBy: AlbumArtistRepository.SortBy, + sortReverse: Boolean, +): Flow> { + val orderBy = when (sortBy) { + AlbumArtistRepository.SortBy.CUSTOM -> "${Artist.TABLE}.${Artist.COLUMN_ID}" + AlbumArtistRepository.SortBy.ARTIST_NAME -> "${Artist.TABLE}.${Artist.COLUMN_NAME}" + AlbumArtistRepository.SortBy.TRACKS_COUNT -> Artist.AlongAttributes.EMBEDDED_TRACKS_COUNT + AlbumArtistRepository.SortBy.ALBUMS_COUNT -> Artist.AlongAttributes.EMBEDDED_ALBUMS_COUNT + } + val orderDirection = if (sortReverse) "DESC" else "ASC" + val query = "SELECT ${Artist.TABLE}.*, " + + "COUNT(${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_SONG_ID}) as ${Artist.AlongAttributes.EMBEDDED_TRACKS_COUNT}, " + + "COUNT(${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID}) as ${Artist.AlongAttributes.EMBEDDED_ALBUMS_COUNT} " + + "FROM ${Artist.TABLE} " + + "LEFT JOIN ${ArtistSongMapping.TABLE} ON ${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_ARTIST_ID} = ${Artist.TABLE}.${Artist.COLUMN_ID} " + + "LEFT JOIN ${AlbumArtistMapping.TABLE} ON ${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ARTIST_ID} = ${Artist.TABLE}.${Artist.COLUMN_ID} " + + "WHERE ${AlbumArtistMapping.COLUMN_IS_ALBUM_ARTIST} = 1 " + + "ORDER BY $orderBy $orderDirection" + return valuesAsFlowRaw(SimpleSQLiteQuery(query)) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumSongMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumSongMappingStore.kt index 901a7028..dcc4e1ab 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumSongMappingStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumSongMappingStore.kt @@ -4,9 +4,22 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import io.github.zyrouge.symphony.services.groove.entities.AlbumSongMapping +import io.github.zyrouge.symphony.services.groove.repositories.SongRepository @Dao interface AlbumSongMappingStore { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun upsert(vararg entities: AlbumSongMapping) } + +fun AlbumSongMappingStore.valuesMappedAsFlow( + songStore: SongStore, + id: String, + sortBy: SongRepository.SortBy, + sortReverse: Boolean, +) = songStore.valuesAsFlow( + sortBy, + sortReverse, + additionalClauseBeforeJoins = "JOIN ${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_ALBUM_ID} = ? ", + additionalArgsBeforeJoins = arrayOf(id), +) diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumStore.kt index 16200123..8290b525 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumStore.kt @@ -10,6 +10,7 @@ import androidx.sqlite.db.SupportSQLiteQuery import io.github.zyrouge.symphony.services.groove.entities.Album import io.github.zyrouge.symphony.services.groove.entities.AlbumArtistMapping import io.github.zyrouge.symphony.services.groove.entities.AlbumSongMapping +import io.github.zyrouge.symphony.services.groove.entities.Artist import io.github.zyrouge.symphony.services.groove.repositories.AlbumRepository import kotlinx.coroutines.flow.Flow @@ -24,6 +25,9 @@ interface AlbumStore { @Query("SELECT * FROM ${Album.TABLE} WHERE ${Album.COLUMN_NAME} = :name LIMIT 1") fun findByName(name: String): Album? + @Query("SELECT * FROM ${Album.TABLE} WHERE ${Album.COLUMN_ID} = :id LIMIT 1") + fun findByIdAsFlow(id: String): Flow + @RawQuery(observedEntities = [Album::class, AlbumArtistMapping::class, AlbumSongMapping::class]) fun valuesAsFlowRaw(query: SupportSQLiteQuery): Flow> } @@ -32,20 +36,30 @@ fun AlbumStore.valuesAsFlow( sortBy: AlbumRepository.SortBy, sortReverse: Boolean, ): Flow> { + val aliasFirstArtist = "firstArtist" + val embeddedArtistName = "firstArtistName" val orderBy = when (sortBy) { AlbumRepository.SortBy.CUSTOM -> "${Album.TABLE}.${Album.COLUMN_ID}" AlbumRepository.SortBy.ALBUM_NAME -> "${Album.TABLE}.${Album.COLUMN_NAME}" + AlbumRepository.SortBy.ARTIST_NAME -> embeddedArtistName AlbumRepository.SortBy.YEAR -> "${Album.TABLE}.${Album.COLUMN_START_YEAR}" AlbumRepository.SortBy.TRACKS_COUNT -> Album.AlongAttributes.EMBEDDED_TRACKS_COUNT AlbumRepository.SortBy.ARTISTS_COUNT -> Album.AlongAttributes.EMBEDDED_ARTISTS_COUNT } val orderDirection = if (sortReverse) "DESC" else "ASC" + val artistQuery = "SELECT" + + "TOP 1 ${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ARTIST_ID} " + + "FROM ${AlbumArtistMapping.TABLE} " + + "WHERE ${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID} = ${Album.COLUMN_ID} " + + "ORDER BY ${AlbumArtistMapping.COLUMN_IS_ALBUM_ARTIST} DESC" val query = "SELECT ${Album.TABLE}.*, " + "COUNT(${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_SONG_ID}) as ${Album.AlongAttributes.EMBEDDED_TRACKS_COUNT}, " + - "COUNT(${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ARTIST_ID}) as ${Album.AlongAttributes.EMBEDDED_ARTISTS_COUNT} " + + "COUNT(${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ARTIST_ID}) as ${Album.AlongAttributes.EMBEDDED_ARTISTS_COUNT}, " + + "$aliasFirstArtist.${Artist.COLUMN_NAME} as $embeddedArtistName" + "FROM ${Album.TABLE} " + "LEFT JOIN ${AlbumSongMapping.TABLE} ON ${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_ALBUM_ID} = ${Album.TABLE}.${Album.COLUMN_ID} " + "LEFT JOIN ${AlbumArtistMapping.TABLE} ON ${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID} = ${Album.TABLE}.${Album.COLUMN_ID} " + + "LEFT JOIN ${Artist.TABLE} $aliasFirstArtist ON ${Artist.TABLE}.${Artist.COLUMN_ID} = ($artistQuery)" + "ORDER BY $orderBy $orderDirection" return valuesAsFlowRaw(SimpleSQLiteQuery(query)) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtworkIndexStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtworkIndexStore.kt index 42563ae9..e0806efd 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtworkIndexStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtworkIndexStore.kt @@ -5,13 +5,13 @@ import androidx.room.Insert import androidx.room.MapColumn import androidx.room.OnConflictStrategy import androidx.room.Query -import io.github.zyrouge.symphony.services.groove.entities.ArtworkIndex +import io.github.zyrouge.symphony.services.groove.entities.SongArtworkIndex @Dao interface ArtworkIndexStore { @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun upsert(vararg entities: ArtworkIndex): List + suspend fun upsert(vararg entities: SongArtworkIndex): List - @Query("SELECT * FROM ${ArtworkIndex.TABLE}") - fun entriesSongIdMapped(): Map<@MapColumn(ArtworkIndex.COLUMN_SONG_ID) String, ArtworkIndex> + @Query("SELECT * FROM ${SongArtworkIndex.TABLE}") + fun entriesSongIdMapped(): Map<@MapColumn(SongArtworkIndex.COLUMN_SONG_ID) String, SongArtworkIndex> } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ComposerStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ComposerStore.kt index 4700ad7d..1819d8e9 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ComposerStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ComposerStore.kt @@ -4,8 +4,15 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.MapColumn import androidx.room.Query +import androidx.room.RawQuery import androidx.room.Update +import androidx.sqlite.db.SimpleSQLiteQuery +import androidx.sqlite.db.SupportSQLiteQuery +import io.github.zyrouge.symphony.services.groove.entities.AlbumComposerMapping import io.github.zyrouge.symphony.services.groove.entities.Composer +import io.github.zyrouge.symphony.services.groove.entities.ComposerSongMapping +import io.github.zyrouge.symphony.services.groove.repositories.ComposerRepository +import kotlinx.coroutines.flow.Flow @Dao interface ComposerStore { @@ -15,8 +22,32 @@ interface ComposerStore { @Update suspend fun update(vararg entities: Composer): Int + @RawQuery(observedEntities = [Composer::class, ComposerSongMapping::class, AlbumComposerMapping::class]) + fun valuesAsFlowRaw(query: SupportSQLiteQuery): Flow> + @Query("SELECT ${Composer.COLUMN_ID}, ${Composer.COLUMN_NAME} FROM ${Composer.TABLE} WHERE ${Composer.COLUMN_NAME} in (:names)") fun entriesByNameNameIdMapped(names: Collection): Map< @MapColumn(Composer.COLUMN_NAME) String, @MapColumn(Composer.COLUMN_ID) String> } + +fun ComposerStore.valuesAsFlow( + sortBy: ComposerRepository.SortBy, + sortReverse: Boolean, +): Flow> { + val orderBy = when (sortBy) { + ComposerRepository.SortBy.CUSTOM -> "${Composer.TABLE}.${Composer.COLUMN_ID}" + ComposerRepository.SortBy.COMPOSER_NAME -> "${Composer.TABLE}.${Composer.COLUMN_NAME}" + ComposerRepository.SortBy.TRACKS_COUNT -> Composer.AlongAttributes.EMBEDDED_TRACKS_COUNT + ComposerRepository.SortBy.ALBUMS_COUNT -> Composer.AlongAttributes.EMBEDDED_ALBUMS_COUNT + } + val orderDirection = if (sortReverse) "DESC" else "ASC" + val query = "SELECT ${Composer.TABLE}.*, " + + "COUNT(${ComposerSongMapping.TABLE}.${ComposerSongMapping.COLUMN_SONG_ID}) as ${Composer.AlongAttributes.EMBEDDED_TRACKS_COUNT}, " + + "COUNT(${AlbumComposerMapping.TABLE}.${AlbumComposerMapping.COLUMN_ALBUM_ID}) as ${Composer.AlongAttributes.EMBEDDED_ALBUMS_COUNT} " + + "FROM ${Composer.TABLE} " + + "LEFT JOIN ${ComposerSongMapping.TABLE} ON ${ComposerSongMapping.TABLE}.${ComposerSongMapping.COLUMN_COMPOSER_ID} = ${Composer.TABLE}.${Composer.COLUMN_ID} " + + "LEFT JOIN ${AlbumComposerMapping.TABLE} ON ${AlbumComposerMapping.TABLE}.${AlbumComposerMapping.COLUMN_COMPOSER_ID} = ${Composer.TABLE}.${Composer.COLUMN_ID}" + + "ORDER BY $orderBy $orderDirection" + return valuesAsFlowRaw(SimpleSQLiteQuery(query)) +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreStore.kt index 6e91259a..3ac131ec 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreStore.kt @@ -4,18 +4,43 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.MapColumn import androidx.room.Query +import androidx.room.RawQuery +import androidx.sqlite.db.SimpleSQLiteQuery +import androidx.sqlite.db.SupportSQLiteQuery import io.github.zyrouge.symphony.services.groove.entities.Genre +import io.github.zyrouge.symphony.services.groove.entities.GenreSongMapping +import io.github.zyrouge.symphony.services.groove.repositories.GenreRepository +import kotlinx.coroutines.flow.Flow @Dao interface GenreStore { @Insert suspend fun insert(vararg entities: Genre): List - @Query("SELECT * FROM ${Genre.TABLE} WHERE ${Genre.COLUMN_NAME} = :name LIMIT 1") - fun findByName(name: String): Genre? + @RawQuery(observedEntities = [Genre::class, GenreSongMapping::class]) + fun valuesAsFlowRaw(query: SupportSQLiteQuery): Flow> @Query("SELECT ${Genre.COLUMN_ID}, ${Genre.COLUMN_NAME} FROM ${Genre.TABLE} WHERE ${Genre.COLUMN_NAME} in (:names)") fun entriesByNameNameIdMapped(names: Collection): Map< @MapColumn(Genre.COLUMN_NAME) String, @MapColumn(Genre.COLUMN_ID) String> } + +fun GenreStore.valuesAsFlow( + sortBy: GenreRepository.SortBy, + sortReverse: Boolean, +): Flow> { + val orderBy = when (sortBy) { + GenreRepository.SortBy.CUSTOM -> "${Genre.TABLE}.${Genre.COLUMN_ID}" + GenreRepository.SortBy.GENRE -> "${Genre.TABLE}.${Genre.COLUMN_NAME}" + GenreRepository.SortBy.TRACKS_COUNT -> Genre.AlongAttributes.EMBEDDED_TRACKS_COUNT + } + val orderDirection = if (sortReverse) "DESC" else "ASC" + val query = "SELECT ${Genre.TABLE}.*, " + + "COUNT(${GenreSongMapping.TABLE}.${GenreSongMapping.COLUMN_SONG_ID}) as ${Genre.AlongAttributes.EMBEDDED_TRACKS_COUNT} " + + "FROM ${Genre.TABLE} " + + "LEFT JOIN ${GenreSongMapping.TABLE} ON ${GenreSongMapping.TABLE}.${GenreSongMapping.COLUMN_GENRE_ID} = ${Genre.TABLE}.${Genre.COLUMN_ID} " + + "LEFT JOIN ${GenreSongMapping.TABLE} ON ${GenreSongMapping.TABLE}.${GenreSongMapping.COLUMN_GENRE_ID} = ${Genre.TABLE}.${Genre.COLUMN_ID} " + + "ORDER BY $orderBy $orderDirection" + return valuesAsFlowRaw(SimpleSQLiteQuery(query)) +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistStore.kt index 307bfbb1..d02cc1bc 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistStore.kt @@ -3,8 +3,13 @@ package io.github.zyrouge.symphony.services.database.store import androidx.room.Dao import androidx.room.Insert import androidx.room.Query +import androidx.room.RawQuery import androidx.room.Update +import androidx.sqlite.db.SimpleSQLiteQuery +import androidx.sqlite.db.SupportSQLiteQuery import io.github.zyrouge.symphony.services.groove.entities.Playlist +import io.github.zyrouge.symphony.services.groove.entities.PlaylistSongMapping +import io.github.zyrouge.symphony.services.groove.repositories.PlaylistRepository import kotlinx.coroutines.flow.Flow @Dao @@ -21,6 +26,24 @@ interface PlaylistStore { @Query("SELECT * FROM ${Playlist.TABLE} WHERE ${Playlist.COLUMN_URI} != NULL") fun valuesLocalOnly(): List - @Query("SELECT * FROM ${Playlist.TABLE}") - fun valuesAsFlow(): Flow> + @RawQuery(observedEntities = [Playlist::class, PlaylistSongMapping::class]) + fun valuesAsFlowRaw(query: SupportSQLiteQuery): Flow> +} + +fun PlaylistStore.valuesAsFlow( + sortBy: PlaylistRepository.SortBy, + sortReverse: Boolean, +): Flow> { + val orderBy = when (sortBy) { + PlaylistRepository.SortBy.CUSTOM -> "${Playlist.TABLE}.${Playlist.COLUMN_ID}" + PlaylistRepository.SortBy.TITLE -> "${Playlist.TABLE}.${Playlist.COLUMN_TITLE}" + PlaylistRepository.SortBy.TRACKS_COUNT -> Playlist.AlongAttributes.EMBEDDED_TRACKS_COUNT + } + val orderDirection = if (sortReverse) "DESC" else "ASC" + val query = "SELECT ${Playlist.TABLE}.*, " + + "COUNT(${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_SONG_ID}) as ${Playlist.AlongAttributes.EMBEDDED_TRACKS_COUNT} " + + "FROM ${Playlist.TABLE} " + + "LEFT JOIN ${PlaylistSongMapping.TABLE} ON ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_PLAYLIST_ID} = ${Playlist.TABLE}.${Playlist.COLUMN_ID} " + + "ORDER BY $orderBy $orderDirection" + return valuesAsFlowRaw(SimpleSQLiteQuery(query)) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongStore.kt index ba3b8219..eb77ce07 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongStore.kt @@ -56,21 +56,23 @@ interface SongStore { fun SongStore.valuesAsFlow( sortBy: SongRepository.SortBy, sortReverse: Boolean, + additionalClauseBeforeJoins: String = "", + additionalArgsBeforeJoins: Array = emptyArray(), ): Flow> { - val aliasAlbumArtist = "albumArtist" - val embeddedArtistName = "artistName" - val embeddedAlbumName = "albumName" - val embeddedAlbumArtistName = "albumArtistName" - val embeddedComposerName = "composerName" + val aliasFirstAlbumArtist = "firstAlbumArtist" + val embeddedFirstArtistName = "firstArtistName" + val embeddedFirstAlbumName = "firstAlbumName" + val embeddedFirstAlbumArtistName = "firstAlbumArtistName" + val embeddedFirstComposerName = "firstComposerName" val orderBy = when (sortBy) { SongRepository.SortBy.CUSTOM -> "${Song.TABLE}.${Song.COLUMN_ID}" SongRepository.SortBy.TITLE -> "${Song.TABLE}.${Song.COLUMN_ID}" - SongRepository.SortBy.ARTIST -> embeddedArtistName - SongRepository.SortBy.ALBUM -> embeddedAlbumName + SongRepository.SortBy.ARTIST -> embeddedFirstArtistName + SongRepository.SortBy.ALBUM -> embeddedFirstAlbumName SongRepository.SortBy.DURATION -> "${Song.TABLE}.${Song.COLUMN_ID}" SongRepository.SortBy.DATE_MODIFIED -> "${Song.TABLE}.${Song.COLUMN_ID}" - SongRepository.SortBy.COMPOSER -> embeddedComposerName - SongRepository.SortBy.ALBUM_ARTIST -> embeddedAlbumArtistName + SongRepository.SortBy.COMPOSER -> embeddedFirstComposerName + SongRepository.SortBy.ALBUM_ARTIST -> embeddedFirstAlbumArtistName SongRepository.SortBy.YEAR -> "${Song.TABLE}.${Song.COLUMN_YEAR}" SongRepository.SortBy.FILENAME -> "${Song.TABLE}.${Song.COLUMN_FILENAME}" SongRepository.SortBy.TRACK_NUMBER -> "${Song.TABLE}.${Song.COLUMN_TRACK_NUMBER}" @@ -93,15 +95,17 @@ fun SongStore.valuesAsFlow( "FROM ${ComposerSongMapping.TABLE} " + "WHERE ${ComposerSongMapping.TABLE}.${ComposerSongMapping.COLUMN_SONG_ID} = ${Song.COLUMN_ID}" val query = "SELECT ${Song.TABLE}.*, " + - "${Artist.TABLE}.${Artist.COLUMN_NAME} as $embeddedArtistName, " + - "${Album.TABLE}.${Album.COLUMN_NAME} as $embeddedAlbumName, " + - "$aliasAlbumArtist.${Artist.COLUMN_NAME} as $embeddedAlbumArtistName, " + - "${Composer.TABLE}.${Composer.COLUMN_NAME} as $embeddedComposerName " + + "${Artist.TABLE}.${Artist.COLUMN_NAME} as $embeddedFirstArtistName, " + + "${Album.TABLE}.${Album.COLUMN_NAME} as $embeddedFirstAlbumName, " + + "$aliasFirstAlbumArtist.${Artist.COLUMN_NAME} as $embeddedFirstAlbumArtistName, " + + "${Composer.TABLE}.${Composer.COLUMN_NAME} as $embeddedFirstComposerName " + "FROM ${Song.TABLE} " + + additionalClauseBeforeJoins + "LEFT JOIN ${Artist.TABLE} ON ${Artist.TABLE}.${Artist.COLUMN_ID} = ($artistQuery)" + "LEFT JOIN ${Album.TABLE} ON ${Album.TABLE}.${Album.COLUMN_ID} = ($albumQuery)" + - "LEFT JOIN ${Artist.TABLE} $aliasAlbumArtist ON ${Artist.TABLE}.${Artist.COLUMN_ID} = ($albumArtistQuery)" + + "LEFT JOIN ${Artist.TABLE} $aliasFirstAlbumArtist ON ${Artist.TABLE}.${Artist.COLUMN_ID} = ($albumArtistQuery)" + "LEFT JOIN ${Composer.TABLE} ON ${Composer.TABLE}.${Composer.COLUMN_ID} = ($composerQuery)" + "ORDER BY $orderBy $orderDirection" - return valuesAsFlowRaw(SimpleSQLiteQuery(query)) + val args = additionalArgsBeforeJoins + return valuesAsFlowRaw(SimpleSQLiteQuery(query, args)) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/MediaExposer.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/MediaExposer.kt index c53746bf..6e31cbb6 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/MediaExposer.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/MediaExposer.kt @@ -8,7 +8,6 @@ import io.github.zyrouge.symphony.services.groove.entities.AlbumArtistMapping import io.github.zyrouge.symphony.services.groove.entities.AlbumSongMapping import io.github.zyrouge.symphony.services.groove.entities.Artist import io.github.zyrouge.symphony.services.groove.entities.ArtistSongMapping -import io.github.zyrouge.symphony.services.groove.entities.ArtworkIndex import io.github.zyrouge.symphony.services.groove.entities.Composer import io.github.zyrouge.symphony.services.groove.entities.ComposerSongMapping import io.github.zyrouge.symphony.services.groove.entities.Genre @@ -19,6 +18,7 @@ import io.github.zyrouge.symphony.services.groove.entities.MediaTreeSongFile import io.github.zyrouge.symphony.services.groove.entities.Playlist import io.github.zyrouge.symphony.services.groove.entities.PlaylistSongMapping import io.github.zyrouge.symphony.services.groove.entities.Song +import io.github.zyrouge.symphony.services.groove.entities.SongArtworkIndex import io.github.zyrouge.symphony.services.groove.entities.SongLyric import io.github.zyrouge.symphony.utils.ActivityUtils import io.github.zyrouge.symphony.utils.ConcurrentSet @@ -86,6 +86,7 @@ class MediaExposer(private val symphony: Symphony) { val playlistId = exPlaylist.id val uri = exPlaylist.uri!! playlistIdsToBeDeletedInMapping.add(playlistId) + ActivityUtils.makePersistableReadableUri(symphony.applicationContext, uri) val extended = Playlist.parse(symphony, playlistId, uri) playlistsToBeUpdated.add(extended.playlist) var nextPlaylistSongMapping: PlaylistSongMapping? = null @@ -118,7 +119,7 @@ class MediaExposer(private val symphony: Symphony) { val songFileStaleIds: ConcurrentSet, val lyricFileStaleIds: ConcurrentSet, val songStaleIds: ConcurrentSet, - val artworkIndexCache: ConcurrentHashMap, + val artworkIndexCache: ConcurrentHashMap, val artworkStaleFiles: ConcurrentSet, val filter: MediaFilter, val songParseOptions: MediaTreeSongFile.ParseOptions, @@ -242,7 +243,7 @@ class MediaExposer(private val symphony: Symphony) { } name } - val artworkIndex = ArtworkIndex(songId = id, file = artworkFile) + val artworkIndex = SongArtworkIndex(songId = id, file = artworkFile) artworkIndex.file?.let { artworkStaleFiles.remove(it) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Album.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Album.kt index 8e8cc586..181adaa5 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Album.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Album.kt @@ -6,7 +6,6 @@ import androidx.room.Embedded import androidx.room.Entity import androidx.room.Index import androidx.room.PrimaryKey -import io.github.zyrouge.symphony.Symphony @Immutable @Entity( @@ -38,16 +37,6 @@ data class Album( } } - fun createArtworkImageRequest(symphony: Symphony) = - symphony.groove.album.createArtworkImageRequest(id) - - // fun getSongIds(symphony: Symphony) = symphony.groove.album.getSongIds(id) -// fun getSortedSongIds(symphony: Symphony) = symphony.groove.song.sort( -// getSongIds(symphony), -// symphony.settings.lastUsedAlbumSongsSortBy.value, -// symphony.settings.lastUsedAlbumSongsSortReverse.value, -// ) - companion object { const val TABLE = "albums" const val COLUMN_ID = "id" diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Genre.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Genre.kt index 18155ca8..076bfedd 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Genre.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Genre.kt @@ -2,10 +2,10 @@ package io.github.zyrouge.symphony.services.groove.entities import androidx.compose.runtime.Immutable import androidx.room.ColumnInfo +import androidx.room.Embedded import androidx.room.Entity import androidx.room.Index import androidx.room.PrimaryKey -import io.github.zyrouge.symphony.Symphony @Immutable @Entity( @@ -19,12 +19,16 @@ data class Genre( @ColumnInfo(COLUMN_NAME) val name: String, ) { - fun getSongIds(symphony: Symphony) = symphony.groove.genre.getSongIds(name) - fun getSortedSongIds(symphony: Symphony) = symphony.groove.song.sort( - getSongIds(symphony), - symphony.settings.lastUsedSongsSortBy.value, - symphony.settings.lastUsedSongsSortReverse.value, - ) + data class AlongAttributes( + @Embedded + val genre: Genre, + @Embedded + val tracksCount: Int, + ) { + companion object { + const val EMBEDDED_TRACKS_COUNT = "tracksCount" + } + } companion object { const val TABLE = "genres" diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Playlist.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Playlist.kt index 155bf2b6..8221fd1f 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Playlist.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Playlist.kt @@ -3,13 +3,12 @@ package io.github.zyrouge.symphony.services.groove.entities import android.net.Uri import androidx.compose.runtime.Immutable import androidx.room.ColumnInfo +import androidx.room.Embedded import androidx.room.Entity import androidx.room.Index import androidx.room.PrimaryKey import io.github.zyrouge.symphony.Symphony -import io.github.zyrouge.symphony.ui.helpers.Assets import io.github.zyrouge.symphony.utils.DocumentFileX -import io.github.zyrouge.symphony.utils.SimplePath import kotlin.io.path.Path import kotlin.io.path.nameWithoutExtension @@ -29,34 +28,18 @@ data class Playlist( @ColumnInfo(COLUMN_PATH) val path: String?, ) { - data class Extended(val playlist: Playlist, val songPaths: List) - - val isLocal get() = uri != null - val isNotLocal get() = uri == null - - fun createArtworkImageRequest(symphony: Symphony) = - getSongIds(symphony).firstOrNull() - ?.let { symphony.groove.song.get(it)?.createArtworkImageRequest(symphony) } - ?: Assets.createPlaceholderImageRequest(symphony) - - fun getSongIds(symphony: Symphony): List { - val parentPath = path?.let { SimplePath(it) }?.parent - val primaryPath = SimplePath(PRIMARY_STORAGE) - return songPaths.mapNotNull { x -> - symphony.groove.song.pathCache[x] - ?: x.takeIf { x[0] == '/' }?.let { - symphony.groove.song.pathCache[it.substring(1).replaceFirst("/", ":")] - } - ?: parentPath?.let { symphony.groove.song.pathCache[it.join(x).pathString] } - ?: symphony.groove.song.pathCache[primaryPath.join(x).pathString] + data class AlongAttributes( + @Embedded + val playlist: Playlist, + @Embedded + val tracksCount: Int, + ) { + companion object { + const val EMBEDDED_TRACKS_COUNT = "tracksCount" } } - fun getSortedSongIds(symphony: Symphony) = symphony.groove.song.sort( - getSongIds(symphony), - symphony.settings.lastUsedPlaylistSongsSortBy.value, - symphony.settings.lastUsedPlaylistSongsSortReverse.value, - ) + data class Parsed(val playlist: Playlist, val songPaths: List) companion object { const val TABLE = "playlists" @@ -66,8 +49,9 @@ data class Playlist( const val COLUMN_PATH = "path" private const val PRIMARY_STORAGE = "primary:" + const val MIMETYPE_M3U = "" - fun parse(symphony: Symphony, playlistId: String?, uri: Uri): Extended { + fun parse(symphony: Symphony, id: String, uri: Uri): Parsed { val file = DocumentFileX.fromSingleUri(symphony.applicationContext, uri)!! val content = symphony.applicationContext.contentResolver.openInputStream(uri) ?.use { String(it.readBytes()) } ?: "" @@ -75,7 +59,6 @@ data class Playlist( .map { it.trim() } .filter { it.isNotEmpty() && it[0] != '#' } .toList() - val id = playlistId ?: symphony.groove.playlist.idGenerator.next() val path = DocumentFileX.getParentPathOfSingleUri(file.uri) ?: file.name val playlist = Playlist( id = id, @@ -83,7 +66,7 @@ data class Playlist( uri = uri, path = path, ) - return Extended(playlist = playlist, songPaths = songPaths) + return Parsed(playlist = playlist, songPaths = songPaths) } } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/ArtworkIndex.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongArtworkIndex.kt similarity index 75% rename from app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/ArtworkIndex.kt rename to app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongArtworkIndex.kt index a7e63d5e..a8a2c81d 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/ArtworkIndex.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongArtworkIndex.kt @@ -9,18 +9,18 @@ import androidx.room.PrimaryKey @Immutable @Entity( - ArtworkIndex.TABLE, + SongArtworkIndex.TABLE, foreignKeys = [ ForeignKey( entity = Song::class, parentColumns = arrayOf(Song.COLUMN_ID), - childColumns = arrayOf(ArtworkIndex.COLUMN_SONG_ID), + childColumns = arrayOf(SongArtworkIndex.COLUMN_SONG_ID), onDelete = ForeignKey.CASCADE, ), ], - indices = [Index(ArtworkIndex.COLUMN_FILE)], + indices = [Index(SongArtworkIndex.COLUMN_FILE)], ) -data class ArtworkIndex( +data class SongArtworkIndex( @PrimaryKey @ColumnInfo(COLUMN_SONG_ID) val songId: String, @@ -28,7 +28,7 @@ data class ArtworkIndex( val file: String?, ) { companion object { - const val TABLE = "artwork_indices" + const val TABLE = "song_artwork_indices" const val COLUMN_SONG_ID = "song_id" const val COLUMN_FILE = "file" } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumArtistRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumArtistRepository.kt index 0dda2ad4..dc4b7ca2 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumArtistRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumArtistRepository.kt @@ -1,9 +1,7 @@ package io.github.zyrouge.symphony.services.groove.repositories import io.github.zyrouge.symphony.Symphony -import io.github.zyrouge.symphony.ui.helpers.Assets -import io.github.zyrouge.symphony.ui.helpers.createHandyImageRequest -import io.github.zyrouge.symphony.utils.withCase +import io.github.zyrouge.symphony.services.database.store.valuesAsFlow class AlbumArtistRepository(private val symphony: Symphony) { enum class SortBy { @@ -13,38 +11,6 @@ class AlbumArtistRepository(private val symphony: Symphony) { ALBUMS_COUNT, } - fun getArtworkUri(albumArtistName: String) = songIdsCache[albumArtistName]?.firstOrNull() - ?.let { symphony.groove.song.getArtworkUri(it) } - ?: symphony.groove.song.getDefaultArtworkUri() - - fun createArtworkImageRequest(albumArtistName: String) = createHandyImageRequest( - symphony.applicationContext, - image = getArtworkUri(albumArtistName), - fallback = Assets.placeholderDarkId, - ) - - fun search(albumArtistNames: List, terms: String, limit: Int = 7) = searcher - .search(terms, albumArtistNames, maxLength = limit) - - fun sort(albumArtistNames: List, by: SortBy, reverse: Boolean): List { - val sensitive = symphony.settings.caseSensitiveSorting.value - val sorted = when (by) { - SortBy.CUSTOM -> albumArtistNames - SortBy.ARTIST_NAME -> albumArtistNames.sortedBy { get(it)?.name?.withCase(sensitive) } - SortBy.TRACKS_COUNT -> albumArtistNames.sortedBy { get(it)?.numberOfTracks } - SortBy.ALBUMS_COUNT -> albumArtistNames.sortedBy { get(it)?.numberOfTracks } - } - return if (reverse) sorted.reversed() else sorted - } - - fun count() = cache.size - fun ids() = cache.keys.toList() - fun values() = cache.values.toList() - - fun get(albumArtistName: String) = cache[albumArtistName] - fun get(albumArtistNames: List) = albumArtistNames.mapNotNull { get(it) } - fun getAlbumIds(albumArtistName: String) = - albumIdsCache[albumArtistName]?.toList() ?: emptyList() - - fun getSongIds(albumArtistName: String) = songIdsCache[albumArtistName]?.toList() ?: emptyList() + fun valuesAsFlow(sortBy: SortBy, sortReverse: Boolean) = + symphony.database.albumArtistMapping.valuesAsFlow(sortBy, sortReverse) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumRepository.kt index 49dc4e4e..443eadfe 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumRepository.kt @@ -1,45 +1,29 @@ package io.github.zyrouge.symphony.services.groove.repositories import io.github.zyrouge.symphony.Symphony -import io.github.zyrouge.symphony.services.database.store.AlbumStore -import io.github.zyrouge.symphony.ui.helpers.Assets -import io.github.zyrouge.symphony.ui.helpers.createHandyImageRequest -import io.github.zyrouge.symphony.utils.withCase +import io.github.zyrouge.symphony.services.database.store.valuesAsFlow +import io.github.zyrouge.symphony.services.database.store.valuesMappedAsFlow class AlbumRepository(private val symphony: Symphony) { enum class SortBy { CUSTOM, ALBUM_NAME, + ARTIST_NAME, TRACKS_COUNT, ARTISTS_COUNT, YEAR, } - fun getArtworkUri(albumId: String) = songIdsCache[albumId]?.firstOrNull() - ?.let { symphony.groove.song.getArtworkUri(it) } - ?: symphony.groove.song.getDefaultArtworkUri() + fun findByIdAsFlow(id: String) = symphony.database.albums.findByIdAsFlow(id) - fun createArtworkImageRequest(albumId: String) = createHandyImageRequest( - symphony.applicationContext, - image = getArtworkUri(albumId), - fallback = Assets.placeholderDarkId, - ) + fun findSongsByIdAsFlow(id: String, sortBy: SongRepository.SortBy, sortReverse: Boolean) = + symphony.database.albumSongMapping.valuesMappedAsFlow( + symphony.database.songs, + id, + sortBy, + sortReverse + ) - fun search(albumIds: List, terms: String, limit: Int = 7) = searcher - .search(terms, albumIds, maxLength = limit) - - fun sort( - albums: List, - by: SortBy, - reverse: Boolean, - ): List { - val sensitive = symphony.settings.caseSensitiveSorting.value - val sorted = when (by) { - SortBy.CUSTOM -> albums - SortBy.ALBUM_NAME -> albums.sortedBy { it.album.name.withCase(sensitive) } - SortBy.TRACKS_COUNT -> albums.sortedBy { it.tracksCount } - SortBy.YEAR -> albums.sortedBy { it.album.startYear } - } - return if (reverse) sorted.reversed() else sorted - } + fun valuesAsFlow(sortBy: SortBy, sortReverse: Boolean) = + symphony.database.albums.valuesAsFlow(sortBy, sortReverse) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/ArtistRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/ArtistRepository.kt index 9dc54a07..8b223612 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/ArtistRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/ArtistRepository.kt @@ -1,19 +1,7 @@ package io.github.zyrouge.symphony.services.groove.repositories import io.github.zyrouge.symphony.Symphony -import io.github.zyrouge.symphony.services.groove.Artist -import io.github.zyrouge.symphony.services.groove.Song -import io.github.zyrouge.symphony.ui.helpers.Assets -import io.github.zyrouge.symphony.ui.helpers.createHandyImageRequest -import io.github.zyrouge.symphony.utils.ConcurrentSet -import io.github.zyrouge.symphony.utils.FuzzySearchOption -import io.github.zyrouge.symphony.utils.FuzzySearcher -import io.github.zyrouge.symphony.utils.concurrentSetOf -import io.github.zyrouge.symphony.utils.withCase -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import java.util.concurrent.ConcurrentHashMap +import io.github.zyrouge.symphony.services.database.store.valuesAsFlow class ArtistRepository(private val symphony: Symphony) { enum class SortBy { @@ -23,92 +11,6 @@ class ArtistRepository(private val symphony: Symphony) { ALBUMS_COUNT, } - private val cache = ConcurrentHashMap() - private val songIdsCache = ConcurrentHashMap>() - private val albumIdsCache = ConcurrentHashMap>() - private val searcher = FuzzySearcher( - options = listOf(FuzzySearchOption({ v -> get(v)?.name?.let { compareString(it) } })) - ) - - val isUpdating get() = symphony.groove.exposer.isUpdating - private val _all = MutableStateFlow>(emptyList()) - val all = _all.asStateFlow() - private val _count = MutableStateFlow(0) - val count = _count.asStateFlow() - - private fun emitCount() = _count.update { - cache.size - } - - internal fun onSong(song: Song) { - song.artists.forEach { artist -> - songIdsCache.compute(artist) { _, value -> - value?.apply { add(song.id) } ?: concurrentSetOf(song.id) - } - var nNumberOfAlbums = 0 - symphony.groove.album.getIdFromSong(song)?.let { album -> - albumIdsCache.compute(artist) { _, value -> - nNumberOfAlbums = (value?.size ?: 0) + 1 - value?.apply { add(album) } ?: concurrentSetOf(album) - } - } - cache.compute(artist) { _, value -> - value?.apply { - numberOfAlbums = nNumberOfAlbums - numberOfTracks++ - } ?: run { - _all.update { - it + artist - } - emitCount() - Artist( - name = artist, - numberOfAlbums = 1, - numberOfTracks = 1, - ) - } - } - } - } - - fun reset() { - cache.clear() - _all.update { - emptyList() - } - emitCount() - } - - fun getArtworkUri(artistName: String) = songIdsCache[artistName]?.firstOrNull() - ?.let { symphony.groove.song.getArtworkUri(it) } - ?: symphony.groove.song.getDefaultArtworkUri() - - fun createArtworkImageRequest(artistName: String) = createHandyImageRequest( - symphony.applicationContext, - image = getArtworkUri(artistName), - fallback = Assets.placeholderDarkId, - ) - - fun search(artistNames: List, terms: String, limit: Int = 7) = searcher - .search(terms, artistNames, maxLength = limit) - - fun sort(artistNames: List, by: SortBy, reverse: Boolean): List { - val sensitive = symphony.settings.caseSensitiveSorting.value - val sorted = when (by) { - SortBy.CUSTOM -> artistNames - SortBy.ARTIST_NAME -> artistNames.sortedBy { get(it)?.name?.withCase(sensitive) } - SortBy.TRACKS_COUNT -> artistNames.sortedBy { get(it)?.numberOfTracks } - SortBy.ALBUMS_COUNT -> artistNames.sortedBy { get(it)?.numberOfTracks } - } - return if (reverse) sorted.reversed() else sorted - } - - fun count() = cache.size - fun ids() = cache.keys.toList() - fun values() = cache.values.toList() - - fun get(id: String) = cache[id] - fun get(ids: List) = ids.mapNotNull { get(it) } - fun getAlbumIds(artistName: String) = albumIdsCache[artistName]?.toList() ?: emptyList() - fun getSongIds(artistName: String) = songIdsCache[artistName]?.toList() ?: emptyList() + fun valuesAsFlow(sortBy: SortBy, sortReverse: Boolean) = + symphony.database.artists.valuesAsFlow(sortBy, sortReverse) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/ComposerRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/ComposerRepository.kt new file mode 100644 index 00000000..21a92995 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/ComposerRepository.kt @@ -0,0 +1,16 @@ +package io.github.zyrouge.symphony.services.groove.repositories + +import io.github.zyrouge.symphony.Symphony +import io.github.zyrouge.symphony.services.database.store.valuesAsFlow + +class ComposerRepository(private val symphony: Symphony) { + enum class SortBy { + CUSTOM, + COMPOSER_NAME, + TRACKS_COUNT, + ALBUMS_COUNT, + } + + fun valuesAsFlow(sortBy: SortBy, sortReverse: Boolean) = + symphony.database.composers.valuesAsFlow(sortBy, sortReverse) +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/GenreRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/GenreRepository.kt index 273a352c..ed4dc71f 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/GenreRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/GenreRepository.kt @@ -1,17 +1,7 @@ package io.github.zyrouge.symphony.services.groove.repositories import io.github.zyrouge.symphony.Symphony -import io.github.zyrouge.symphony.services.groove.Genre -import io.github.zyrouge.symphony.services.groove.Song -import io.github.zyrouge.symphony.utils.ConcurrentSet -import io.github.zyrouge.symphony.utils.FuzzySearchOption -import io.github.zyrouge.symphony.utils.FuzzySearcher -import io.github.zyrouge.symphony.utils.concurrentSetOf -import io.github.zyrouge.symphony.utils.withCase -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import java.util.concurrent.ConcurrentHashMap +import io.github.zyrouge.symphony.services.database.store.valuesAsFlow class GenreRepository(private val symphony: Symphony) { enum class SortBy { @@ -20,70 +10,6 @@ class GenreRepository(private val symphony: Symphony) { TRACKS_COUNT, } - private val cache = ConcurrentHashMap() - private val songIdsCache = ConcurrentHashMap>() - private val searcher = FuzzySearcher( - options = listOf(FuzzySearchOption({ v -> get(v)?.name?.let { compareString(it) } })) - ) - - val isUpdating get() = symphony.groove.exposer.isUpdating - private val _all = MutableStateFlow>(emptyList()) - val all = _all.asStateFlow() - private val _count = MutableStateFlow(0) - val count = _count.asStateFlow() - - private fun emitCount() = _count.update { - cache.size - } - - internal fun onSong(song: Song) { - song.genres.forEach { genre -> - songIdsCache.compute(genre) { _, value -> - value?.apply { add(song.id) } ?: concurrentSetOf(song.id) - } - cache.compute(genre) { _, value -> - value?.apply { - numberOfTracks++ - } ?: run { - _all.update { - it + genre - } - emitCount() - Genre( - name = genre, - numberOfTracks = 1, - ) - } - } - } - } - - fun reset() { - cache.clear() - songIdsCache.clear() - _all.update { - emptyList() - } - emitCount() - } - - fun search(genreNames: List, terms: String, limit: Int = 7) = searcher - .search(terms, genreNames, maxLength = limit) - - fun sort(genreNames: List, by: SortBy, reverse: Boolean): List { - val sensitive = symphony.settings.caseSensitiveSorting.value - val sorted = when (by) { - SortBy.CUSTOM -> genreNames - SortBy.GENRE -> genreNames.sortedBy { get(it)?.name?.withCase(sensitive) } - SortBy.TRACKS_COUNT -> genreNames.sortedBy { get(it)?.numberOfTracks } - } - return if (reverse) sorted.reversed() else sorted - } - - fun count() = cache.size - fun ids() = cache.keys.toList() - fun values() = cache.values.toList() - - fun get(id: String) = cache[id] - fun getSongIds(genre: String) = songIdsCache[genre]?.toList() ?: emptyList() + fun valuesAsFlow(sortBy: SortBy, sortReverse: Boolean) = + symphony.database.genres.valuesAsFlow(sortBy, sortReverse) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/PlaylistRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/PlaylistRepository.kt index 9482329d..93995ddd 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/PlaylistRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/PlaylistRepository.kt @@ -1,21 +1,7 @@ package io.github.zyrouge.symphony.services.groove.repositories -import android.net.Uri import io.github.zyrouge.symphony.Symphony -import io.github.zyrouge.symphony.services.groove.Playlist -import io.github.zyrouge.symphony.utils.ActivityUtils -import io.github.zyrouge.symphony.utils.FuzzySearchOption -import io.github.zyrouge.symphony.utils.FuzzySearcher -import io.github.zyrouge.symphony.utils.KeyGenerator -import io.github.zyrouge.symphony.utils.Logger -import io.github.zyrouge.symphony.utils.mutate -import io.github.zyrouge.symphony.utils.withCase -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import java.io.FileNotFoundException -import java.util.concurrent.ConcurrentHashMap +import io.github.zyrouge.symphony.services.database.store.valuesAsFlow class PlaylistRepository(private val symphony: Symphony) { enum class SortBy { @@ -24,221 +10,8 @@ class PlaylistRepository(private val symphony: Symphony) { TRACKS_COUNT, } - private val cache = ConcurrentHashMap() - internal val idGenerator = KeyGenerator.TimeCounterRandomMix() - private val searcher = FuzzySearcher( - options = listOf(FuzzySearchOption({ v -> get(v)?.title?.let { compareString(it) } })) - ) - - private val _isUpdating = MutableStateFlow(false) - val isUpdating = _isUpdating.asStateFlow() - private val _updateId = MutableStateFlow(0L) - val updateId = _updateId.asStateFlow() - private val _all = MutableStateFlow>(emptyList()) - val all = _all.asStateFlow() - private val _count = MutableStateFlow(0) - val count = _count.asStateFlow() - private val _favorites = MutableStateFlow>(emptyList()) - val favorites = _favorites.asStateFlow() - - private fun emitUpdate(value: Boolean) = _isUpdating.update { - value - } - - private fun emitUpdateId() = _updateId.update { - System.currentTimeMillis() - } - - private fun emitCount() = _count.update { - cache.size - } - - suspend fun fetch() { - emitUpdate(true) - try { - val context = symphony.applicationContext - val playlists = symphony.database.playlists.entries() - playlists.values.map { x -> - val playlist = when { - x.isLocal -> { - ActivityUtils.makePersistableReadableUri(context, x.uri!!) - Playlist.parse(symphony, x.id, x.uri) - } - - else -> x - } - cache[playlist.id] = playlist - _all.update { - it + playlist.id - } - emitUpdateId() - emitCount() - } - if (!cache.containsKey(FAVORITE_PLAYLIST)) { - add(getFavorites()) - } - } catch (_: FileNotFoundException) { - } catch (err: Exception) { - Logger.error("PlaylistRepository", "fetch failed", err) - } - _favorites.update { - getFavorites().getSongIds(symphony) - } - emitUpdateId() - emitUpdate(false) - } - - fun reset() { - emitUpdate(true) - cache.clear() - _all.update { - emptyList() - } - emitCount() - _favorites.update { - emptyList() - } - emitUpdateId() - emitUpdate(false) - } - - fun search(playlistIds: List, terms: String, limit: Int = 7) = searcher - .search(terms, playlistIds, maxLength = limit) - - fun sort(playlistIds: List, by: SortBy, reverse: Boolean): List { - val sensitive = symphony.settings.caseSensitiveSorting.value - val sorted = when (by) { - SortBy.CUSTOM -> { - val prefix = listOfNotNull(FAVORITE_PLAYLIST) - val others = playlistIds.toMutableList() - prefix.forEach { others.remove(it) } - prefix + others - } - - SortBy.TITLE -> playlistIds.sortedBy { get(it)?.title?.withCase(sensitive) } - SortBy.TRACKS_COUNT -> playlistIds.sortedBy { get(it)?.numberOfTracks } - } - return if (reverse) sorted.reversed() else sorted - } - - fun count() = cache.size - fun ids() = cache.keys.toList() - fun values() = cache.values.toList() - - fun get(id: String) = cache[id] - fun get(ids: List) = ids.mapNotNull { get(it) } - - fun getFavorites() = cache[FAVORITE_PLAYLIST] - ?: create(FAVORITE_PLAYLIST, "Favorites", emptyList()) - - fun create(title: String, songIds: List) = create(idGenerator.next(), title, songIds) - private fun create(id: String, title: String, songIds: List) = Playlist( - id = id, - title = title, - songPaths = songIds.mapNotNull { symphony.groove.song.get(it)?.path }, - uri = null, - path = null, - ) - - fun add(playlist: Playlist) { - cache[playlist.id] = playlist - _all.update { - it + playlist.id - } - emitUpdateId() - emitCount() - symphony.groove.coroutineScope.launch { - symphony.database.playlists.insert(playlist) - } - } - - fun delete(id: String) { - Logger.error( - "PlaylistRepository", - "cache ${cache.containsKey(id)}" - ) - cache.remove(id)?.uri?.let { - runCatching { - ActivityUtils.makePersistableReadableUri(symphony.applicationContext, it) - } - } - _all.update { - it - id - } - emitUpdateId() - emitCount() - symphony.groove.coroutineScope.launch { - symphony.database.playlists.delete(id) - } - } - - fun update(id: String, songIds: List) { - val playlist = get(id) ?: return - val updated = Playlist( - id = id, - title = playlist.title, - songPaths = songIds.mapNotNull { symphony.groove.song.get(it)?.path }, - uri = playlist.uri, - path = playlist.path, - ) - cache[id] = updated - emitUpdateId() - emitCount() - if (id == FAVORITE_PLAYLIST) { - _favorites.update { - songIds - } - } - symphony.groove.coroutineScope.launch { - symphony.database.playlists.update(updated) - } - } - - // NOTE: maybe we shouldn't use groove's coroutine scope? - fun favorite(songId: String) { - val favorites = getFavorites() - val songIds = favorites.getSongIds(symphony) - if (songIds.contains(songId)) { - return - } - update(favorites.id, songIds.mutate { add(songId) }) - } - - fun unfavorite(songId: String) { - val favorites = getFavorites() - val songIds = favorites.getSongIds(symphony) - if (!songIds.contains(songId)) { - return - } - update(favorites.id, songIds.mutate { remove(songId) }) - } - - fun isFavoritesPlaylist(playlist: Playlist) = playlist.id == FAVORITE_PLAYLIST - fun isBuiltInPlaylist(playlist: Playlist) = isFavoritesPlaylist(playlist) - - fun savePlaylistToUri(playlist: Playlist, uri: Uri) { - val outputStream = symphony.applicationContext.contentResolver.openOutputStream(uri, "w") - outputStream?.use { - val content = playlist.songPaths.joinToString("\n") - it.write(content.toByteArray()) - } - } - - fun renamePlaylist(playlist: Playlist, title: String) { - val renamed = playlist.withTitle(title) - cache[playlist.id] = renamed - emitUpdateId() - symphony.groove.coroutineScope.launch { - symphony.database.playlists.update(renamed) - } - } - - internal fun onScanFinish() { - _favorites.update { - getFavorites().getSongIds(symphony) - } - emitUpdateId() - } + fun valuesAsFlow(sortBy: SortBy, sortReverse: Boolean) = + symphony.database.playlists.valuesAsFlow(sortBy, sortReverse) companion object { private const val FAVORITE_PLAYLIST = "favorites" diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/SongRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/SongRepository.kt index 2dcfbcc3..051ef6d0 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/SongRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/SongRepository.kt @@ -1,25 +1,7 @@ package io.github.zyrouge.symphony.services.groove.repositories -import android.net.Uri -import androidx.core.net.toUri import io.github.zyrouge.symphony.Symphony -import io.github.zyrouge.symphony.services.groove.MediaExposer -import io.github.zyrouge.symphony.services.groove.Song -import io.github.zyrouge.symphony.services.groove.SongFile -import io.github.zyrouge.symphony.ui.helpers.Assets -import io.github.zyrouge.symphony.ui.helpers.createHandyImageRequest -import io.github.zyrouge.symphony.utils.FuzzySearchOption -import io.github.zyrouge.symphony.utils.FuzzySearcher -import io.github.zyrouge.symphony.utils.Logger -import io.github.zyrouge.symphony.utils.SimpleFileSystem -import io.github.zyrouge.symphony.utils.SimplePath -import io.github.zyrouge.symphony.utils.joinToStringIfNotEmpty -import io.github.zyrouge.symphony.utils.withCase -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import java.util.concurrent.ConcurrentHashMap -import kotlin.io.path.Path +import io.github.zyrouge.symphony.services.database.store.valuesAsFlow class SongRepository(private val symphony: Symphony) { enum class SortBy { @@ -36,112 +18,6 @@ class SongRepository(private val symphony: Symphony) { TRACK_NUMBER, } - internal val pathCache = ConcurrentHashMap() - private val searcher = FuzzySearcher( - options = listOf( - FuzzySearchOption({ v -> get(v)?.title?.let { compareString(it) } }, 3), - FuzzySearchOption({ v -> get(v)?.filename?.let { compareString(it) } }, 2), - FuzzySearchOption({ v -> get(v)?.artists?.let { compareCollection(it) } }), - FuzzySearchOption({ v -> get(v)?.album?.let { compareString(it) } }) - ) - ) - - val isUpdating get() = symphony.groove.exposer.isUpdating - private val _all = MutableStateFlow>(emptyList()) - val all = _all.asStateFlow() - private val _count = MutableStateFlow(0) - val count = _count.asStateFlow() - private val _id = MutableStateFlow(System.currentTimeMillis()) - val id = _id.asStateFlow() - var explorer = SimpleFileSystem.Folder() - - private fun emitCount() = _count.update { cache.size } - - private fun emitIds() = _id.update { - System.currentTimeMillis() - } - - internal fun onSongFile(state: MediaExposer.SongFileState, songFile: SongFile) { - when (state) { - MediaExposer.SongFileState.New -> { - Path(songFile.path).fileName - } - } - } - - fun reset() { - cache.clear() - pathCache.clear() - explorer = SimpleFileSystem.Folder() - emitIds() - _all.update { - emptyList() - } - emitCount() - } - - fun search(songIds: List, terms: String, limit: Int = 7) = searcher - .search(terms, songIds, maxLength = limit) - - fun sort(songIds: List, by: SortBy, reverse: Boolean): List { - val sensitive = symphony.settings.caseSensitiveSorting.value - val sorted = when (by) { - SortBy.CUSTOM -> songIds - SortBy.TITLE -> songIds.sortedBy { get(it)?.title?.withCase(sensitive) } - SortBy.ARTIST -> songIds.sortedBy { get(it)?.artists?.joinToStringIfNotEmpty(sensitive) } - SortBy.ALBUM -> songIds.sortedBy { get(it)?.album?.withCase(sensitive) } - SortBy.DURATION -> songIds.sortedBy { get(it)?.duration } - SortBy.DATE_MODIFIED -> songIds.sortedBy { get(it)?.dateModified } - SortBy.COMPOSER -> songIds.sortedBy { - get(it)?.composers?.joinToStringIfNotEmpty(sensitive) - } - - SortBy.ALBUM_ARTIST -> songIds.sortedBy { - get(it)?.albumArtists?.joinToStringIfNotEmpty(sensitive) - } - - SortBy.YEAR -> songIds.sortedBy { get(it)?.year } - SortBy.FILENAME -> songIds.sortedBy { get(it)?.filename?.withCase(sensitive) } - SortBy.TRACK_NUMBER -> songIds.sortedWith( - compareBy({ get(it)?.discNumber }, { get(it)?.trackNumber }), - ) - } - return if (reverse) sorted.reversed() else sorted - } - - fun count() = cache.size - fun ids() = cache.keys.toList() - fun values() = cache.values.toList() - - fun get(id: String) = cache[id] - fun get(ids: List) = ids.mapNotNull { get(it) } - - fun getArtworkUri(songId: String): Uri = get(songId)?.coverFile - ?.let { symphony.database.artworkCache.get(it) }?.toUri() - ?: getDefaultArtworkUri() - - fun getDefaultArtworkUri() = Assets.getPlaceholderUri(symphony) - - fun createArtworkImageRequest(songId: String) = createHandyImageRequest( - symphony.applicationContext, - image = getArtworkUri(songId), - fallback = Assets.getPlaceholderId(symphony), - ) - - suspend fun getLyrics(song: Song): String? { - try { - val lrcPath = SimplePath(song.path).let { - it.parent?.join(it.nameWithoutExtension + ".lrc")?.pathString - } - symphony.groove.exposer.uris[lrcPath]?.let { uri -> - symphony.applicationContext.contentResolver.openInputStream(uri)?.use { - return String(it.readBytes()) - } - } - return symphony.database.lyricsCache.get(song.id) - } catch (err: Exception) { - Logger.error("LyricsRepository", "fetch lyrics failed", err) - } - return null - } + fun valuesAsFlow(sortBy: SortBy, sortReverse: Boolean) = + symphony.database.songs.valuesAsFlow(sortBy, sortReverse) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumArtistGrid.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumArtistGrid.kt index 1b2b0755..4597620a 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumArtistGrid.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumArtistGrid.kt @@ -14,6 +14,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import io.github.zyrouge.symphony.services.groove.Groove +import io.github.zyrouge.symphony.services.groove.entities.Artist import io.github.zyrouge.symphony.services.groove.repositories.AlbumArtistRepository import io.github.zyrouge.symphony.ui.helpers.ViewContext @@ -21,16 +22,10 @@ import io.github.zyrouge.symphony.ui.helpers.ViewContext @Composable fun AlbumArtistGrid( context: ViewContext, - albumArtistNames: List, - albumArtistsCount: Int? = null, + albumArtists: List, + sortBy: AlbumArtistRepository.SortBy, + sortReverse: Boolean, ) { - val sortBy by context.symphony.settings.lastUsedAlbumArtistsSortBy.flow.collectAsState() - val sortReverse by context.symphony.settings.lastUsedAlbumArtistsSortReverse.flow.collectAsState() - val sortedAlbumArtistNames by remember(albumArtistNames, sortBy, sortReverse) { - derivedStateOf { - context.symphony.groove.albumArtist.sort(albumArtistNames, sortBy, sortReverse) - } - } val horizontalGridColumns by context.symphony.settings.lastUsedAlbumArtistsHorizontalGridColumns.flow.collectAsState() val verticalGridColumns by context.symphony.settings.lastUsedAlbumArtistsVerticalGridColumns.flow.collectAsState() val gridColumns by remember(horizontalGridColumns, verticalGridColumns) { @@ -56,9 +51,7 @@ fun AlbumArtistGrid( }, label = { Text( - context.symphony.t.XArtists( - (albumArtistsCount ?: albumArtistNames.size).toString() - ) + context.symphony.t.XArtists(albumArtists.size.toString()) ) }, onShowModifyLayout = { @@ -68,7 +61,7 @@ fun AlbumArtistGrid( }, content = { when { - albumArtistNames.isEmpty() -> IconTextBody( + albumArtists.isEmpty() -> IconTextBody( icon = { modifier -> Icon( Icons.Filled.Person, @@ -81,14 +74,11 @@ fun AlbumArtistGrid( else -> ResponsiveGrid(gridColumns) { itemsIndexed( - sortedAlbumArtistNames, + albumArtists, key = { i, x -> "$i-$x" }, contentType = { _, _ -> Groove.Kind.ARTIST } - ) { _, albumArtistName -> - context.symphony.groove.albumArtist.get(albumArtistName) - ?.let { albumArtist -> - AlbumArtistTile(context, albumArtist) - } + ) { _, albumArtist -> + AlbumArtistTile(context, albumArtist) } } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumGrid.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumGrid.kt index ebc38ff4..03a85388 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumGrid.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumGrid.kt @@ -20,14 +20,12 @@ import io.github.zyrouge.symphony.ui.helpers.ViewContext @OptIn(ExperimentalMaterial3Api::class) @Composable -fun AlbumGrid(context: ViewContext, albums: List) { - val sortBy by context.symphony.settings.lastUsedAlbumsSortBy.flow.collectAsState() - val sortReverse by context.symphony.settings.lastUsedAlbumsSortReverse.flow.collectAsState() - val sortedAlbumIds by remember(albums, sortBy, sortReverse) { - derivedStateOf { - context.symphony.groove.album.sort(albums, sortBy, sortReverse) - } - } +fun AlbumGrid( + context: ViewContext, + albums: List, + sortBy: AlbumRepository.SortBy, + sortReverse: Boolean, +) { val horizontalGridColumns by context.symphony.settings.lastUsedAlbumsHorizontalGridColumns.flow.collectAsState() val verticalGridColumns by context.symphony.settings.lastUsedAlbumsVerticalGridColumns.flow.collectAsState() val gridColumns by remember(horizontalGridColumns, verticalGridColumns) { @@ -53,7 +51,7 @@ fun AlbumGrid(context: ViewContext, albums: List) { context.symphony.settings.lastUsedAlbumsSortBy.setValue(it) }, label = { - Text(context.symphony.t.XAlbums((albums.size).toString())) + Text(context.symphony.t.XAlbums(albums.size.toString())) }, onShowModifyLayout = { showModifyLayoutSheet = true @@ -75,7 +73,7 @@ fun AlbumGrid(context: ViewContext, albums: List) { else -> ResponsiveGrid(gridColumns) { itemsIndexed( - sortedAlbumIds, + albums, key = { i, x -> "$i-$x" }, contentType = { _, _ -> Groove.Kind.ALBUM } ) { _, album -> @@ -110,5 +108,6 @@ private fun AlbumRepository.SortBy.label(context: ViewContext) = when (this) { AlbumRepository.SortBy.ALBUM_NAME -> context.symphony.t.Album AlbumRepository.SortBy.ARTIST_NAME -> context.symphony.t.Artist AlbumRepository.SortBy.TRACKS_COUNT -> context.symphony.t.TrackCount + AlbumRepository.SortBy.ARTISTS_COUNT -> context.symphony.t.ArtistCount AlbumRepository.SortBy.YEAR -> context.symphony.t.Year } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/ArtistGrid.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/ArtistGrid.kt index 371a6672..40dc1f26 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/ArtistGrid.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/ArtistGrid.kt @@ -14,6 +14,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import io.github.zyrouge.symphony.services.groove.Groove +import io.github.zyrouge.symphony.services.groove.entities.Artist import io.github.zyrouge.symphony.services.groove.repositories.ArtistRepository import io.github.zyrouge.symphony.ui.helpers.ViewContext @@ -21,16 +22,10 @@ import io.github.zyrouge.symphony.ui.helpers.ViewContext @Composable fun ArtistGrid( context: ViewContext, - artistName: List, - artistsCount: Int? = null, + artists: List, + sortBy: ArtistRepository.SortBy, + sortReverse: Boolean, ) { - val sortBy by context.symphony.settings.lastUsedArtistsSortBy.flow.collectAsState() - val sortReverse by context.symphony.settings.lastUsedArtistsSortReverse.flow.collectAsState() - val sortedArtistNames by remember(artistName, sortBy, sortReverse) { - derivedStateOf { - context.symphony.groove.artist.sort(artistName, sortBy, sortReverse) - } - } val horizontalGridColumns by context.symphony.settings.lastUsedArtistsHorizontalGridColumns.flow.collectAsState() val verticalGridColumns by context.symphony.settings.lastUsedArtistsVerticalGridColumns.flow.collectAsState() val gridColumns by remember(horizontalGridColumns, verticalGridColumns) { @@ -55,7 +50,7 @@ fun ArtistGrid( context.symphony.settings.lastUsedArtistsSortBy.setValue(it) }, label = { - Text(context.symphony.t.XArtists((artistsCount ?: artistName.size).toString())) + Text(context.symphony.t.XArtists(artists.size.toString())) }, onShowModifyLayout = { showModifyLayoutSheet = true @@ -64,7 +59,7 @@ fun ArtistGrid( }, content = { when { - artistName.isEmpty() -> IconTextBody( + artists.isEmpty() -> IconTextBody( icon = { modifier -> Icon( Icons.Filled.Person, @@ -77,13 +72,11 @@ fun ArtistGrid( else -> ResponsiveGrid(gridColumns) { itemsIndexed( - sortedArtistNames, + artists, key = { i, x -> "$i-$x" }, contentType = { _, _ -> Groove.Kind.ARTIST } - ) { _, artistName -> - context.symphony.groove.artist.get(artistName)?.let { artist -> - ArtistTile(context, artist) - } + ) { _, artist -> + ArtistTile(context, artist) } } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenreGrid.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenreGrid.kt index 22f65246..d7e99a51 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenreGrid.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenreGrid.kt @@ -35,6 +35,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import io.github.zyrouge.symphony.services.groove.Groove +import io.github.zyrouge.symphony.services.groove.entities.Genre import io.github.zyrouge.symphony.services.groove.repositories.GenreRepository import io.github.zyrouge.symphony.ui.helpers.ViewContext import io.github.zyrouge.symphony.ui.view.GenreViewRoute @@ -66,16 +67,10 @@ private object GenreTile { @Composable fun GenreGrid( context: ViewContext, - genreNames: List, - genresCount: Int? = null, + attributedGenres: List, + sortBy: GenreRepository.SortBy, + sortReverse: Boolean, ) { - val sortBy by context.symphony.settings.lastUsedGenresSortBy.flow.collectAsState() - val sortReverse by context.symphony.settings.lastUsedGenresSortReverse.flow.collectAsState() - val sortedGenreNames by remember(genreNames, sortBy, sortReverse) { - derivedStateOf { - context.symphony.groove.genre.sort(genreNames, sortBy, sortReverse) - } - } val horizontalGridColumns by context.symphony.settings.lastUsedGenresHorizontalGridColumns.flow.collectAsState() val verticalGridColumns by context.symphony.settings.lastUsedGenresVerticalGridColumns.flow.collectAsState() val gridColumns by remember(horizontalGridColumns, verticalGridColumns) { @@ -101,11 +96,7 @@ fun GenreGrid( context.symphony.settings.lastUsedGenresSortBy.setValue(it) }, label = { - Text( - context.symphony.t.XGenres( - (genresCount ?: genreNames.size).toString() - ) - ) + Text(context.symphony.t.XGenres(attributedGenres.size.toString())) }, onShowModifyLayout = { showModifyLayoutSheet = true @@ -115,7 +106,7 @@ fun GenreGrid( }, content = { when { - genreNames.isEmpty() -> IconTextBody( + attributedGenres.isEmpty() -> IconTextBody( icon = { modifier -> Icon( Icons.Filled.MusicNote, @@ -128,67 +119,16 @@ fun GenreGrid( else -> ResponsiveGrid(gridColumns) { gridData -> itemsIndexed( - sortedGenreNames, + attributedGenres, key = { i, x -> "$i-$x" }, contentType = { _, _ -> Groove.Kind.GENRE } - ) { i, genreName -> - context.symphony.groove.genre.get(genreName)?.let { genre -> - Card( - modifier = Modifier - .height(IntrinsicSize.Min) - .padding( - start = if (i % gridData.columnsCount == 0) 12.dp else 0.dp, - end = if ((i - 1) % gridData.columnsCount == 0) 12.dp else 8.dp, - bottom = 8.dp, - ), - colors = GenreTile.cardColors(i), - onClick = { - context.navController.navigate(GenreViewRoute(genre.name)) - } - ) { - Box( - modifier = Modifier - .fillMaxSize() - .defaultMinSize(minHeight = 88.dp), - contentAlignment = Alignment.Center, - ) { - Box( - modifier = Modifier - .align(Alignment.BottomStart) - .matchParentSize() - .fillMaxWidth() - .alpha(0.25f) - .absoluteOffset(8.dp, 12.dp) - ) { - Text( - genre.name, - textAlign = TextAlign.Start, - style = MaterialTheme.typography.displaySmall - .copy(fontWeight = FontWeight.Bold), - softWrap = false, - overflow = TextOverflow.Clip, - ) - } - Column( - modifier = Modifier.padding(8.dp, 16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Text( - genre.name, - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodyLarge - .copy(fontWeight = FontWeight.Bold), - ) - Text( - context.symphony.t.XSongs(genre.numberOfTracks.toString()), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.labelSmall, - ) - } - } - } - } + ) { i, attributedGenre -> + GenreTile( + context, + index = i, + columnsCount = gridData.columnsCount, + attributedGenre = attributedGenre, + ) } } } @@ -214,6 +154,70 @@ fun GenreGrid( ) } +@Composable +private fun GenreTile( + context: ViewContext, + index: Int, + columnsCount: Int, + attributedGenre: Genre.AlongAttributes, +) { + Card( + modifier = Modifier + .height(IntrinsicSize.Min) + .padding( + start = if (index % columnsCount == 0) 12.dp else 0.dp, + end = if ((index - 1) % columnsCount == 0) 12.dp else 8.dp, + bottom = 8.dp, + ), + colors = GenreTile.cardColors(index), + onClick = { + context.navController.navigate(GenreViewRoute(attributedGenre.genre.name)) + } + ) { + Box( + modifier = Modifier + .fillMaxSize() + .defaultMinSize(minHeight = 88.dp), + contentAlignment = Alignment.Center, + ) { + Box( + modifier = Modifier + .align(Alignment.BottomStart) + .matchParentSize() + .fillMaxWidth() + .alpha(0.25f) + .absoluteOffset(8.dp, 12.dp) + ) { + Text( + attributedGenre.genre.name, + textAlign = TextAlign.Start, + style = MaterialTheme.typography.displaySmall + .copy(fontWeight = FontWeight.Bold), + softWrap = false, + overflow = TextOverflow.Clip, + ) + } + Column( + modifier = Modifier.padding(8.dp, 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text( + attributedGenre.genre.name, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyLarge + .copy(fontWeight = FontWeight.Bold), + ) + Text( + context.symphony.t.XSongs(attributedGenre.tracksCount.toString()), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.labelSmall, + ) + } + } + } +} + private fun GenreRepository.SortBy.label(context: ViewContext) = when (this) { GenreRepository.SortBy.CUSTOM -> context.symphony.t.Custom GenreRepository.SortBy.GENRE -> context.symphony.t.Genre diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/PlaylistGrid.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/PlaylistGrid.kt index 33d255f7..5721d58c 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/PlaylistGrid.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/PlaylistGrid.kt @@ -15,6 +15,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import io.github.zyrouge.symphony.services.groove.Groove +import io.github.zyrouge.symphony.services.groove.entities.Playlist import io.github.zyrouge.symphony.services.groove.repositories.PlaylistRepository import io.github.zyrouge.symphony.ui.helpers.ViewContext @@ -22,17 +23,11 @@ import io.github.zyrouge.symphony.ui.helpers.ViewContext @Composable fun PlaylistGrid( context: ViewContext, - playlistIds: List, - playlistsCount: Int? = null, + playlists: List, + sortBy: PlaylistRepository.SortBy, + sortReverse: Boolean, leadingContent: @Composable () -> Unit = {}, ) { - val sortBy by context.symphony.settings.lastUsedPlaylistsSortBy.flow.collectAsState() - val sortReverse by context.symphony.settings.lastUsedPlaylistsSortReverse.flow.collectAsState() - val sortedPlaylistIds by remember(playlistIds, sortBy, sortReverse) { - derivedStateOf { - context.symphony.groove.playlist.sort(playlistIds, sortBy, sortReverse) - } - } val horizontalGridColumns by context.symphony.settings.lastUsedPlaylistsHorizontalGridColumns.flow.collectAsState() val verticalGridColumns by context.symphony.settings.lastUsedPlaylistsVerticalGridColumns.flow.collectAsState() val gridColumns by remember(horizontalGridColumns, verticalGridColumns) { @@ -59,11 +54,7 @@ fun PlaylistGrid( context.symphony.settings.lastUsedPlaylistsSortBy.setValue(it) }, label = { - Text( - context.symphony.t.XPlaylists( - (playlistsCount ?: playlistIds.size).toString() - ) - ) + Text(context.symphony.t.XPlaylists(playlists.size.toString())) }, onShowModifyLayout = { showModifyLayoutSheet = true @@ -73,7 +64,7 @@ fun PlaylistGrid( }, content = { when { - playlistIds.isEmpty() -> IconTextBody( + playlists.isEmpty() -> IconTextBody( icon = { modifier -> Icon( Icons.AutoMirrored.Filled.QueueMusic, @@ -88,13 +79,11 @@ fun PlaylistGrid( else -> ResponsiveGrid(gridColumns) { itemsIndexed( - sortedPlaylistIds, + playlists, key = { i, x -> "$i-$x" }, contentType = { _, _ -> Groove.Kind.PLAYLIST } - ) { _, playlistId -> - context.symphony.groove.playlist.get(playlistId)?.let { playlist -> - PlaylistTile(context, playlist) - } + ) { _, playlist -> + PlaylistTile(context, playlist) } } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongList.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongList.kt index d122d46b..5bbc0ae5 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongList.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongList.kt @@ -15,15 +15,11 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import io.github.zyrouge.symphony.services.groove.Groove -import io.github.zyrouge.symphony.services.groove.Song +import io.github.zyrouge.symphony.services.groove.entities.Song import io.github.zyrouge.symphony.services.groove.repositories.SongRepository import io.github.zyrouge.symphony.services.radio.Radio import io.github.zyrouge.symphony.ui.helpers.ViewContext @@ -39,25 +35,17 @@ enum class SongListType { @Composable fun SongList( context: ViewContext, - songIds: List, - songsCount: Int? = null, + songs: List, + sortBy: SongRepository.SortBy, + sortReverse: Boolean, leadingContent: (LazyListScope.() -> Unit)? = null, trailingContent: (LazyListScope.() -> Unit)? = null, trailingOptionsContent: (@Composable ColumnScope.(Int, Song, () -> Unit) -> Unit)? = null, cardThumbnailLabel: (@Composable (Int, Song) -> Unit)? = null, cardThumbnailLabelStyle: SongCardThumbnailLabelStyle = SongCardThumbnailLabelStyle.Default, - type: SongListType = SongListType.Default, disableHeartIcon: Boolean = false, enableAddMediaFoldersHint: Boolean = false, ) { - val sortBy by type.getLastUsedSortBy(context).flow.collectAsState() - val sortReverse by type.getLastUsedSortReverse(context).flow.collectAsState() - val sortedSongIds by remember(songIds, sortBy, sortReverse) { - derivedStateOf { - context.symphony.groove.song.sort(songIds, sortBy, sortReverse) - } - } - MediaSortBarScaffold( mediaSortBar = { MediaSortBar( @@ -73,16 +61,16 @@ fun SongList( type.setLastUsedSortBy(context, it) }, label = { - Text(context.symphony.t.XSongs((songsCount ?: songIds.size).toString())) + Text(context.symphony.t.XSongs(songs.size.toString())) }, onShufflePlay = { - context.symphony.radio.shorty.playQueue(sortedSongIds, shuffle = true) + context.symphony.radio.shorty.playQueue(songs, shuffle = true) } ) }, content = { when { - songIds.isEmpty() -> IconTextBody( + songs.isEmpty() -> IconTextBody( icon = { modifier -> Icon(Icons.Filled.MusicNote, null, modifier = modifier) }, @@ -115,28 +103,26 @@ fun SongList( ) { leadingContent?.invoke(this) itemsIndexed( - sortedSongIds, + songs, key = { i, x -> "$i-$x" }, contentType = { _, _ -> Groove.Kind.SONG } - ) { i, songId -> - context.symphony.groove.song.get(songId)?.let { song -> - SongCard( - context, - song = song, - thumbnailLabel = cardThumbnailLabel?.let { - { it(i, song) } - }, - thumbnailLabelStyle = cardThumbnailLabelStyle, - disableHeartIcon = disableHeartIcon, - trailingOptionsContent = trailingOptionsContent?.let { - { onDismissRequest -> it(i, song, onDismissRequest) } - }, - ) { - context.symphony.radio.shorty.playQueue( - sortedSongIds, - Radio.PlayOptions(index = i) - ) - } + ) { i, song -> + SongCard( + context, + song = song, + thumbnailLabel = cardThumbnailLabel?.let { + { it(i, song) } + }, + thumbnailLabelStyle = cardThumbnailLabelStyle, + disableHeartIcon = disableHeartIcon, + trailingOptionsContent = trailingOptionsContent?.let { + { onDismissRequest -> it(i, song, onDismissRequest) } + }, + ) { + context.symphony.radio.shorty.playQueue( + sortedSongIds, + Radio.PlayOptions(index = i) + ) } } trailingContent?.invoke(this) diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Album.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Album.kt index 5fcf4ae0..f96e1fea 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Album.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Album.kt @@ -27,9 +27,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -37,7 +35,7 @@ import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import io.github.zyrouge.symphony.services.groove.Album +import io.github.zyrouge.symphony.services.groove.entities.Album import io.github.zyrouge.symphony.ui.components.AlbumDropdownMenu import io.github.zyrouge.symphony.ui.components.AnimatedNowPlayingBottomBar import io.github.zyrouge.symphony.ui.components.GenericGrooveBanner @@ -45,28 +43,37 @@ import io.github.zyrouge.symphony.ui.components.IconButtonPlaceholder import io.github.zyrouge.symphony.ui.components.IconTextBody import io.github.zyrouge.symphony.ui.components.SongCardThumbnailLabelStyle import io.github.zyrouge.symphony.ui.components.SongList -import io.github.zyrouge.symphony.ui.components.SongListType import io.github.zyrouge.symphony.ui.components.TopAppBarMinimalTitle import io.github.zyrouge.symphony.ui.helpers.ViewContext +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.transformLatest import kotlinx.serialization.Serializable @Serializable data class AlbumViewRoute(val albumId: String) -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalCoroutinesApi::class) @Composable fun AlbumView(context: ViewContext, route: AlbumViewRoute) { - val allAlbumIds by context.symphony.groove.album.all.collectAsState() - val allSongIds by context.symphony.groove.song.all.collectAsState() - val album by remember(allAlbumIds) { - derivedStateOf { context.symphony.groove.album.get(route.albumId) } - } - val songIds by remember(album, allSongIds) { - derivedStateOf { album?.getSongIds(context.symphony) ?: listOf() } - } - val isViable by remember(allAlbumIds) { - derivedStateOf { allAlbumIds.contains(route.albumId) } - } + val albumFlow = context.symphony.groove.album.findByIdAsFlow(route.albumId) + val album by albumFlow.collectAsState(null) + val songsSortBy by context.symphony.settings.lastUsedAlbumSongsSortBy.flow.collectAsState() + val songsSortReverse by context.symphony.settings.lastUsedAlbumSongsSortReverse.flow.collectAsState() + val songs by albumFlow + .transformLatest { album -> + val value = when { + album == null -> emptyFlow() + else -> context.symphony.groove.album.findSongsByIdAsFlow( + album.id, + songsSortBy, + songsSortReverse, + ).transformLatest { emit(it.map { x -> x.song }) } + } + emitAll(value) + } + .collectAsState(emptyList()) Scaffold( modifier = Modifier.fillMaxSize(), @@ -100,11 +107,12 @@ fun AlbumView(context: ViewContext, route: AlbumViewRoute) { .padding(contentPadding) .fillMaxSize() ) { - if (isViable) { - SongList( + when { + album != null -> SongList( context, - songIds = songIds, - type = SongListType.Album, + songs = songs, + sortBy = songsSortBy, + sortReverse = songsSortReverse, leadingContent = { item { AlbumHero(context, album!!) @@ -115,7 +123,9 @@ fun AlbumView(context: ViewContext, route: AlbumViewRoute) { }, cardThumbnailLabelStyle = SongCardThumbnailLabelStyle.Subtle, ) - } else UnknownAlbum(context, route.albumId) + + else -> UnknownAlbum(context, route.albumId) + } } }, bottomBar = { diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/AlbumArtists.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/AlbumArtists.kt index 45d54e11..3dca8f47 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/AlbumArtists.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/AlbumArtists.kt @@ -6,18 +6,25 @@ import androidx.compose.runtime.getValue import io.github.zyrouge.symphony.ui.components.AlbumArtistGrid import io.github.zyrouge.symphony.ui.components.LoaderScaffold import io.github.zyrouge.symphony.ui.helpers.ViewContext +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.transformLatest +@OptIn(ExperimentalCoroutinesApi::class) @Composable fun AlbumArtistsView(context: ViewContext) { - val isUpdating by context.symphony.groove.albumArtist.isUpdating.collectAsState() - val albumArtistNames by context.symphony.groove.albumArtist.all.collectAsState() - val albumArtistsCount by context.symphony.groove.albumArtist.count.collectAsState() + val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsState() + val sortBy by context.symphony.settings.lastUsedAlbumArtistsSortBy.flow.collectAsState() + val sortReverse by context.symphony.settings.lastUsedAlbumArtistsSortReverse.flow.collectAsState() + val albumArtists by context.symphony.groove.albumArtist.valuesAsFlow(sortBy, sortReverse) + .transformLatest { emit(it.map { x -> x.artist }) } + .collectAsState(emptyList()) LoaderScaffold(context, isLoading = isUpdating) { AlbumArtistGrid( context, - albumArtistNames = albumArtistNames, - albumArtistsCount = albumArtistsCount, + albumArtists = albumArtists, + sortBy = sortBy, + sortReverse = sortReverse, ) } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Albums.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Albums.kt index a4f3c0c5..c3acb2a7 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Albums.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Albums.kt @@ -6,13 +6,25 @@ import androidx.compose.runtime.getValue import io.github.zyrouge.symphony.ui.components.AlbumGrid import io.github.zyrouge.symphony.ui.components.LoaderScaffold import io.github.zyrouge.symphony.ui.helpers.ViewContext +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.transformLatest +@OptIn(ExperimentalCoroutinesApi::class) @Composable fun AlbumsView(context: ViewContext) { val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsState() - val albums by context.symphony.database.albums.valuesAsFlow().collectAsState(emptyList()) + val sortBy by context.symphony.settings.lastUsedAlbumsSortBy.flow.collectAsState() + val sortReverse by context.symphony.settings.lastUsedAlbumsSortReverse.flow.collectAsState() + val albums by context.symphony.groove.album.valuesAsFlow(sortBy, sortReverse) + .transformLatest { emit(it.map { x -> x.album }) } + .collectAsState(emptyList()) LoaderScaffold(context, isLoading = isUpdating) { - AlbumGrid(context, albums = albums) + AlbumGrid( + context, + albums = albums, + sortBy = sortBy, + sortReverse = sortReverse, + ) } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Artists.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Artists.kt index 717cf400..f37aab90 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Artists.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Artists.kt @@ -6,18 +6,25 @@ import androidx.compose.runtime.getValue import io.github.zyrouge.symphony.ui.components.ArtistGrid import io.github.zyrouge.symphony.ui.components.LoaderScaffold import io.github.zyrouge.symphony.ui.helpers.ViewContext +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.transformLatest +@OptIn(ExperimentalCoroutinesApi::class) @Composable fun ArtistsView(context: ViewContext) { - val isUpdating by context.symphony.groove.artist.isUpdating.collectAsState() - val artistNames by context.symphony.groove.artist.all.collectAsState() - val artistsCount by context.symphony.groove.artist.count.collectAsState() + val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsState() + val sortBy by context.symphony.settings.lastUsedArtistsSortBy.flow.collectAsState() + val sortReverse by context.symphony.settings.lastUsedArtistsSortReverse.flow.collectAsState() + val artists by context.symphony.groove.artist.valuesAsFlow(sortBy, sortReverse) + .transformLatest { emit(it.map { x -> x.artist }) } + .collectAsState(emptyList()) LoaderScaffold(context, isLoading = isUpdating) { ArtistGrid( context, - artistName = artistNames, - artistsCount = artistsCount, + artists = artists, + sortBy = sortBy, + sortReverse = sortReverse, ) } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Genres.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Genres.kt index cd1c24cd..4ae49cb8 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Genres.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Genres.kt @@ -9,15 +9,18 @@ import io.github.zyrouge.symphony.ui.helpers.ViewContext @Composable fun GenresView(context: ViewContext) { - val isUpdating by context.symphony.groove.genre.isUpdating.collectAsState() - val genreNames by context.symphony.groove.genre.all.collectAsState() - val genresCount by context.symphony.groove.genre.count.collectAsState() + val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsState() + val sortBy by context.symphony.settings.lastUsedGenresSortBy.flow.collectAsState() + val sortReverse by context.symphony.settings.lastUsedGenresSortReverse.flow.collectAsState() + val attributedGenres by context.symphony.groove.genre.valuesAsFlow(sortBy, sortReverse) + .collectAsState(emptyList()) LoaderScaffold(context, isLoading = isUpdating) { GenreGrid( context, - genreNames = genreNames, - genresCount = genresCount, + attributedGenres = attributedGenres, + sortBy = sortBy, + sortReverse = sortReverse, ) } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Playlists.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Playlists.kt index 67792c96..6211970f 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Playlists.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Playlists.kt @@ -25,20 +25,26 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import io.github.zyrouge.symphony.services.groove.MediaExposer import io.github.zyrouge.symphony.services.groove.Playlist +import io.github.zyrouge.symphony.services.groove.entities.Playlist import io.github.zyrouge.symphony.ui.components.LoaderScaffold import io.github.zyrouge.symphony.ui.components.NewPlaylistDialog import io.github.zyrouge.symphony.ui.components.PlaylistGrid import io.github.zyrouge.symphony.ui.helpers.ViewContext import io.github.zyrouge.symphony.utils.ActivityUtils import io.github.zyrouge.symphony.utils.Logger +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.transformLatest +@OptIn(ExperimentalCoroutinesApi::class) @Composable fun PlaylistsView(context: ViewContext) { - val isUpdating by context.symphony.groove.playlist.isUpdating.collectAsState() - val playlists by context.symphony.groove.playlist.all.collectAsState() - val playlistsCount by context.symphony.groove.playlist.count.collectAsState() + val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsState() + val sortBy by context.symphony.settings.lastUsedPlaylistsSortBy.flow.collectAsState() + val sortReverse by context.symphony.settings.lastUsedPlaylistsSortReverse.flow.collectAsState() + val playlists by context.symphony.groove.playlist.valuesAsFlow(sortBy, sortReverse) + .transformLatest { emit(it.map { x -> x.playlist }) } + .collectAsState(emptyList()) var showPlaylistCreator by remember { mutableStateOf(false) } val openPlaylistLauncher = rememberLauncherForActivityResult( @@ -63,8 +69,9 @@ fun PlaylistsView(context: ViewContext) { LoaderScaffold(context, isLoading = isUpdating) { PlaylistGrid( context, - playlistIds = playlists, - playlistsCount = playlistsCount, + playlists = playlists, + sortBy = sortBy, + sortReverse = sortReverse, leadingContent = { PlaylistControlBar( context, @@ -72,7 +79,7 @@ fun PlaylistsView(context: ViewContext) { showPlaylistCreator = true }, showPlaylistPicker = { - openPlaylistLauncher.launch(arrayOf(MediaExposer.MIMETYPE_M3U)) + openPlaylistLauncher.launch(arrayOf(Playlist.MIMETYPE_M3U)) }, ) Spacer(modifier = Modifier.height(4.dp)) diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Songs.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Songs.kt index 150c0276..282b3fcd 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Songs.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Songs.kt @@ -6,18 +6,25 @@ import androidx.compose.runtime.getValue import io.github.zyrouge.symphony.ui.components.LoaderScaffold import io.github.zyrouge.symphony.ui.components.SongList import io.github.zyrouge.symphony.ui.helpers.ViewContext +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.transformLatest +@OptIn(ExperimentalCoroutinesApi::class) @Composable fun SongsView(context: ViewContext) { - val isUpdating by context.symphony.groove.song.isUpdating.collectAsState() - val songIds by context.symphony.groove.song.all.collectAsState() - val songsCount by context.symphony.groove.song.count.collectAsState() + val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsState() + val sortBy by context.symphony.settings.lastUsedSongsSortBy.flow.collectAsState() + val sortReverse by context.symphony.settings.lastUsedSongsSortReverse.flow.collectAsState() + val songs by context.symphony.groove.song.valuesAsFlow(sortBy, sortReverse) + .transformLatest { emit(it.map { x -> x.song }) } + .collectAsState(emptyList()) LoaderScaffold(context, isLoading = isUpdating) { SongList( context, - songIds = songIds, - songsCount = songsCount, + songs = songs, + sortBy = sortBy, + sortReverse = sortReverse, enableAddMediaFoldersHint = true, ) } From 6c6f73c82fedffce973ee11c437185d743bbd3da Mon Sep 17 00:00:00 2001 From: Zyrouge Date: Mon, 10 Feb 2025 23:16:39 +0530 Subject: [PATCH 05/15] refactor: mapping table queries --- .../symphony/services/database/Database.kt | 8 ++- .../services/database/PersistentDatabase.kt | 26 +++++--- .../store/AlbumComposerMappingStore.kt | 13 ++++ .../database/store/ArtistSongMappingStore.kt | 13 ++++ .../store/ComposerSongMappingStore.kt | 13 ++++ .../database/store/GenreSongMappingStore.kt | 13 ++++ .../store/PlaylistSongMappingStore.kt | 35 ++++++++++ ...IndexStore.kt => SongArtworkIndexStore.kt} | 7 +- .../{ArtworkStore.kt => SongArtworkStore.kt} | 4 +- .../store/SongQueueSongMappingStore.kt | 60 +++++++++++++++++ .../services/database/store/SongQueueStore.kt | 31 +++++++++ .../services/database/store/SongStore.kt | 65 ++++++++++++++++--- .../symphony/services/groove/MediaExposer.kt | 26 ++++---- .../symphony/services/groove/entities/Song.kt | 11 +++- .../groove/entities/SongArtworkIndex.kt | 5 +- .../services/groove/entities/SongQueue.kt | 50 ++++++++++++++ .../groove/entities/SongQueueSongMapping.kt | 60 +++++++++++++++++ .../groove/repositories/SongRepository.kt | 14 ++++ .../github/zyrouge/symphony/ui/view/Album.kt | 4 +- .../symphony/ui/view/home/AlbumArtists.kt | 4 +- .../zyrouge/symphony/ui/view/home/Albums.kt | 4 +- .../zyrouge/symphony/ui/view/home/Artists.kt | 4 +- .../zyrouge/symphony/ui/view/home/Browser.kt | 3 +- .../symphony/ui/view/home/Playlists.kt | 4 +- 24 files changed, 424 insertions(+), 53 deletions(-) rename app/src/main/java/io/github/zyrouge/symphony/services/database/store/{ArtworkIndexStore.kt => SongArtworkIndexStore.kt} (63%) rename app/src/main/java/io/github/zyrouge/symphony/services/database/store/{ArtworkStore.kt => SongArtworkStore.kt} (83%) create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueSongMappingStore.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueStore.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueue.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueueSongMapping.kt diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/Database.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/Database.kt index ee28f597..e76a5321 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/Database.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/Database.kt @@ -1,7 +1,7 @@ package io.github.zyrouge.symphony.services.database import io.github.zyrouge.symphony.Symphony -import io.github.zyrouge.symphony.services.database.store.ArtworkStore +import io.github.zyrouge.symphony.services.database.store.SongArtworkStore import io.github.zyrouge.symphony.utils.KeyGenerator class Database(symphony: Symphony) { @@ -24,8 +24,6 @@ class Database(symphony: Symphony) { val albumSongMapping get() = persistent.albumSongMapping() val artists get() = persistent.artists() val artistSongMapping get() = persistent.artistSongMapping() - val artworks = ArtworkStore(symphony) - val artworkIndices get() = persistent.artworkIndices() val composers get() = persistent.composers() val composerSongMapping get() = persistent.composerSongMapping() val genres get() = persistent.genre() @@ -35,6 +33,10 @@ class Database(symphony: Symphony) { val mediaTreeSongFiles get() = persistent.mediaTreeSongFiles() val playlists get() = persistent.playlists() val playlistSongMapping get() = persistent.playlistSongMapping() + val songArtworks = SongArtworkStore(symphony) + val songArtworkIndices get() = persistent.songArtworkIndices() val songLyrics get() = persistent.songLyrics() + val songQueueSongMapping get() = persistent.songQueueSongMapping() + val songQueue get() = persistent.songQueue() val songs get() = persistent.songs() } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/PersistentDatabase.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/PersistentDatabase.kt index 525bdf0d..14056bbb 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/PersistentDatabase.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/PersistentDatabase.kt @@ -10,7 +10,6 @@ import io.github.zyrouge.symphony.services.database.store.AlbumSongMappingStore import io.github.zyrouge.symphony.services.database.store.AlbumStore import io.github.zyrouge.symphony.services.database.store.ArtistSongMappingStore import io.github.zyrouge.symphony.services.database.store.ArtistStore -import io.github.zyrouge.symphony.services.database.store.ArtworkIndexStore import io.github.zyrouge.symphony.services.database.store.ComposerSongMappingStore import io.github.zyrouge.symphony.services.database.store.ComposerStore import io.github.zyrouge.symphony.services.database.store.GenreSongMappingStore @@ -20,7 +19,10 @@ import io.github.zyrouge.symphony.services.database.store.MediaTreeLyricFileStor import io.github.zyrouge.symphony.services.database.store.MediaTreeSongFileStore import io.github.zyrouge.symphony.services.database.store.PlaylistSongMappingStore import io.github.zyrouge.symphony.services.database.store.PlaylistStore +import io.github.zyrouge.symphony.services.database.store.SongArtworkIndexStore import io.github.zyrouge.symphony.services.database.store.SongLyricStore +import io.github.zyrouge.symphony.services.database.store.SongQueueSongMappingStore +import io.github.zyrouge.symphony.services.database.store.SongQueueStore import io.github.zyrouge.symphony.services.database.store.SongStore import io.github.zyrouge.symphony.services.groove.entities.Album import io.github.zyrouge.symphony.services.groove.entities.AlbumArtistMapping @@ -40,29 +42,33 @@ import io.github.zyrouge.symphony.services.groove.entities.PlaylistSongMapping import io.github.zyrouge.symphony.services.groove.entities.Song import io.github.zyrouge.symphony.services.groove.entities.SongArtworkIndex import io.github.zyrouge.symphony.services.groove.entities.SongLyric +import io.github.zyrouge.symphony.services.groove.entities.SongQueue +import io.github.zyrouge.symphony.services.groove.entities.SongQueueSongMapping import io.github.zyrouge.symphony.utils.RoomConvertors @Database( version = 1, entities = [ - Album::class, AlbumArtistMapping::class, AlbumComposerMapping::class, AlbumSongMapping::class, - Artist::class, + Album::class, ArtistSongMapping::class, - SongArtworkIndex::class, - Composer::class, + Artist::class, ComposerSongMapping::class, - Genre::class, + Composer::class, GenreSongMapping::class, + Genre::class, MediaTreeFolder::class, MediaTreeLyricFile::class, MediaTreeSongFile::class, - Playlist::class, PlaylistSongMapping::class, - Song::class, + Playlist::class, + SongArtworkIndex::class, SongLyric::class, + SongQueueSongMapping::class, + SongQueue::class, + Song::class, ], ) @TypeConverters(RoomConvertors::class) @@ -73,7 +79,6 @@ abstract class PersistentDatabase : RoomDatabase() { abstract fun albums(): AlbumStore abstract fun artistSongMapping(): ArtistSongMappingStore abstract fun artists(): ArtistStore - abstract fun artworkIndices(): ArtworkIndexStore abstract fun composerSongMapping(): ComposerSongMappingStore abstract fun composers(): ComposerStore abstract fun genreSongMapping(): GenreSongMappingStore @@ -83,7 +88,10 @@ abstract class PersistentDatabase : RoomDatabase() { abstract fun mediaTreeSongFiles(): MediaTreeSongFileStore abstract fun playlistSongMapping(): PlaylistSongMappingStore abstract fun playlists(): PlaylistStore + abstract fun songArtworkIndices(): SongArtworkIndexStore abstract fun songLyrics(): SongLyricStore + abstract fun songQueueSongMapping(): SongQueueSongMappingStore + abstract fun songQueue(): SongQueueStore abstract fun songs(): SongStore companion object { diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumComposerMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumComposerMappingStore.kt index 7f91d25d..0fb1ab56 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumComposerMappingStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumComposerMappingStore.kt @@ -4,9 +4,22 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import io.github.zyrouge.symphony.services.groove.entities.AlbumComposerMapping +import io.github.zyrouge.symphony.services.groove.repositories.SongRepository @Dao interface AlbumComposerMappingStore { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun upsert(vararg entities: AlbumComposerMapping) } + +fun AlbumComposerMappingStore.valuesMappedAsFlow( + songStore: SongStore, + id: String, + sortBy: SongRepository.SortBy, + sortReverse: Boolean, +) = songStore.valuesAsFlow( + sortBy, + sortReverse, + additionalClauseBeforeJoins = "JOIN ${AlbumComposerMapping.TABLE}.${AlbumComposerMapping.COLUMN_COMPOSER_ID} = ? ", + additionalArgsBeforeJoins = arrayOf(id), +) diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistSongMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistSongMappingStore.kt index e6aa8a80..a699becc 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistSongMappingStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistSongMappingStore.kt @@ -4,9 +4,22 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import io.github.zyrouge.symphony.services.groove.entities.ArtistSongMapping +import io.github.zyrouge.symphony.services.groove.repositories.SongRepository @Dao interface ArtistSongMappingStore { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun upsert(vararg entities: ArtistSongMapping) } + +fun ArtistSongMappingStore.valuesMappedAsFlow( + songStore: SongStore, + id: String, + sortBy: SongRepository.SortBy, + sortReverse: Boolean, +) = songStore.valuesAsFlow( + sortBy, + sortReverse, + additionalClauseBeforeJoins = "JOIN ${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_ARTIST_ID} = ? ", + additionalArgsBeforeJoins = arrayOf(id), +) diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ComposerSongMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ComposerSongMappingStore.kt index 31309535..5196de53 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ComposerSongMappingStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ComposerSongMappingStore.kt @@ -4,9 +4,22 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import io.github.zyrouge.symphony.services.groove.entities.ComposerSongMapping +import io.github.zyrouge.symphony.services.groove.repositories.SongRepository @Dao interface ComposerSongMappingStore { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun upsert(vararg entities: ComposerSongMapping) } + +fun ComposerSongMappingStore.valuesMappedAsFlow( + songStore: SongStore, + id: String, + sortBy: SongRepository.SortBy, + sortReverse: Boolean, +) = songStore.valuesAsFlow( + sortBy, + sortReverse, + additionalClauseBeforeJoins = "JOIN ${ComposerSongMapping.TABLE}.${ComposerSongMapping.COLUMN_COMPOSER_ID} = ? ", + additionalArgsBeforeJoins = arrayOf(id), +) diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreSongMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreSongMappingStore.kt index ef701ff3..64210ff1 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreSongMappingStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreSongMappingStore.kt @@ -4,9 +4,22 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import io.github.zyrouge.symphony.services.groove.entities.GenreSongMapping +import io.github.zyrouge.symphony.services.groove.repositories.SongRepository @Dao interface GenreSongMappingStore { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun upsert(vararg entities: GenreSongMapping) } + +fun GenreSongMappingStore.valuesMappedAsFlow( + songStore: SongStore, + id: String, + sortBy: SongRepository.SortBy, + sortReverse: Boolean, +) = songStore.valuesAsFlow( + sortBy, + sortReverse, + additionalClauseBeforeJoins = "JOIN ${GenreSongMapping.TABLE}.${GenreSongMapping.COLUMN_GENRE_ID} = ? ", + additionalArgsBeforeJoins = arrayOf(id), +) diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistSongMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistSongMappingStore.kt index 7d01fbbf..4f8a5479 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistSongMappingStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistSongMappingStore.kt @@ -4,6 +4,11 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.Query import io.github.zyrouge.symphony.services.groove.entities.PlaylistSongMapping +import io.github.zyrouge.symphony.services.groove.entities.Song +import io.github.zyrouge.symphony.services.groove.repositories.SongRepository +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.mapLatest @Dao interface PlaylistSongMappingStore { @@ -13,3 +18,33 @@ interface PlaylistSongMappingStore { @Query("DELETE FROM ${PlaylistSongMapping.TABLE} WHERE ${PlaylistSongMapping.COLUMN_PLAYLIST_ID} IN (:ids)") suspend fun deletePlaylistIds(ids: Collection) } + +@OptIn(ExperimentalCoroutinesApi::class) +fun PlaylistSongMappingStore.valuesMappedAsFlow( + songStore: SongStore, + id: String, + sortBy: SongRepository.SortBy, + sortReverse: Boolean, +): Flow> { + val query = songStore.valuesAsFlowQuery( + sortBy, + sortReverse, + additionalClauseBeforeJoins = "JOIN ${PlaylistSongMapping.TABLE} ON ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_PLAYLIST_ID} = ? AND ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_SONG_ID} = ${Song.COLUMN_ID} ", + additionalArgsBeforeJoins = arrayOf(id), + ) + val entries = songStore.entriesAsPlaylistSongMappedFlowRaw(query) + return entries.mapLatest { + val list = mutableListOf() + var head = it.firstNotNullOfOrNull { + when { + it.value.mapping.isStart -> it.value + else -> null + } + } + while (head != null) { + list.add(head.song) + head = it[head.mapping.nextId] + } + list.toList() + } +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtworkIndexStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongArtworkIndexStore.kt similarity index 63% rename from app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtworkIndexStore.kt rename to app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongArtworkIndexStore.kt index e0806efd..37dd1f17 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtworkIndexStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongArtworkIndexStore.kt @@ -8,10 +8,13 @@ import androidx.room.Query import io.github.zyrouge.symphony.services.groove.entities.SongArtworkIndex @Dao -interface ArtworkIndexStore { +interface SongArtworkIndexStore { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun upsert(vararg entities: SongArtworkIndex): List - @Query("SELECT * FROM ${SongArtworkIndex.TABLE}") + @Query("SELECT * FROM ${SongArtworkIndex.TABLE} WHERE ${SongArtworkIndex.COLUMN_SONG_ID} = :songId LIMIT 1") + fun findBySongId(songId: String): SongArtworkIndex? + + @Query("SELECT * FROM ${SongArtworkIndex.TABLE} WHERE ${SongArtworkIndex.COLUMN_SONG_ID} != null") fun entriesSongIdMapped(): Map<@MapColumn(SongArtworkIndex.COLUMN_SONG_ID) String, SongArtworkIndex> } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtworkStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongArtworkStore.kt similarity index 83% rename from app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtworkStore.kt rename to app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongArtworkStore.kt index 80f777c9..7e10c43d 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtworkStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongArtworkStore.kt @@ -4,8 +4,8 @@ import io.github.zyrouge.symphony.Symphony import io.github.zyrouge.symphony.services.database.adapters.FileTreeDatabaseAdapter import java.nio.file.Paths -class ArtworkStore(val symphony: Symphony) { - private val path = Paths.get(symphony.applicationContext.dataDir.absolutePath, "covers") +class SongArtworkStore(val symphony: Symphony) { + private val path = Paths.get(symphony.applicationContext.dataDir.absolutePath, "song_artworks") private val adapter = FileTreeDatabaseAdapter(path.toFile()) fun get(key: String) = adapter.get(key) diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueSongMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueSongMappingStore.kt new file mode 100644 index 00000000..dc3bde3f --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueSongMappingStore.kt @@ -0,0 +1,60 @@ +package io.github.zyrouge.symphony.services.database.store + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.MapColumn +import androidx.room.Query +import androidx.room.RawQuery +import androidx.sqlite.db.SimpleSQLiteQuery +import androidx.sqlite.db.SupportSQLiteQuery +import io.github.zyrouge.symphony.services.groove.entities.Song +import io.github.zyrouge.symphony.services.groove.entities.SongQueueSongMapping +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.mapLatest + +@Dao +interface SongQueueSongMappingStore { + @Insert + suspend fun insert(vararg entities: SongQueueSongMapping) + + @Query("DELETE FROM ${SongQueueSongMapping.TABLE} WHERE ${SongQueueSongMapping.COLUMN_QUEUE_ID} IN (:ids)") + suspend fun deleteSongQueueIds(ids: Collection) + + @RawQuery(observedEntities = [Song::class, SongQueueSongMapping::class]) + fun entriesAsFlowRaw(query: SupportSQLiteQuery): Flow> +} + +fun SongQueueSongMappingStore.entriesAsFlow(queueId: String): Flow> { + val query = "SELECT ${Song.TABLE}.*, " + + "${SongQueueSongMapping.TABLE}.* " + + "FROM ${SongQueueSongMapping.TABLE} " + + "WHERE ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_QUEUE_ID} = ? " + + "LEFT JOIN ${Song.TABLE} ON ${Song.TABLE}.${Song.COLUMN_ID} = ${SongQueueSongMapping.COLUMN_SONG_ID} " + + "ORDER BY ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_IS_HEAD} DESC" + val args = arrayOf(queueId) + return entriesAsFlowRaw(SimpleSQLiteQuery(query, args)) +} + +@OptIn(ExperimentalCoroutinesApi::class) +fun SongQueueSongMappingStore.transformEntriesAsValuesFlow(entries: Flow>): Flow> { + return entries.mapLatest { + val list = mutableListOf() + var head = it.firstNotNullOfOrNull { + when { + it.value.mapping.isStart -> it.value + else -> null + } + } + while (head != null) { + list.add(head.song) + head = it[head.mapping.nextId] + } + list.toList() + } +} + +fun SongQueueSongMappingStore.valuesAsFlow(queueId: String): Flow> { + val entries = entriesAsFlow(queueId) + return transformEntriesAsValuesFlow(entries) +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueStore.kt new file mode 100644 index 00000000..26f149ed --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueStore.kt @@ -0,0 +1,31 @@ +package io.github.zyrouge.symphony.services.database.store + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import androidx.room.RawQuery +import androidx.sqlite.db.SimpleSQLiteQuery +import androidx.sqlite.db.SupportSQLiteQuery +import io.github.zyrouge.symphony.services.groove.entities.SongQueue +import io.github.zyrouge.symphony.services.groove.entities.SongQueueSongMapping +import kotlinx.coroutines.flow.Flow + +@Dao +interface SongQueueStore { + @Insert + suspend fun insert(vararg entities: SongQueue): List + + @Query("DELETE FROM ${SongQueue.TABLE} WHERE ${SongQueue.COLUMN_ID} = :id") + suspend fun delete(id: String): Int + + @RawQuery(observedEntities = [SongQueue::class, SongQueueSongMapping::class]) + fun valuesAsFlowRaw(query: SupportSQLiteQuery): Flow> +} + +fun SongQueueStore.valuesAsFlow(): Flow> { + val query = "SELECT ${SongQueue.TABLE}.*, " + + "COUNT(${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_SONG_ID}) as ${SongQueue.AlongAttributes.EMBEDDED_TRACKS_COUNT} " + + "FROM ${SongQueue.TABLE} " + + "LEFT JOIN ${SongQueueSongMapping.TABLE} ON ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_QUEUE_ID} = ${SongQueue.TABLE}.${SongQueue.COLUMN_ID}" + return valuesAsFlowRaw(SimpleSQLiteQuery(query)) +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongStore.kt index eb77ce07..ad84baf3 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongStore.kt @@ -2,6 +2,7 @@ package io.github.zyrouge.symphony.services.database.store import androidx.room.Dao import androidx.room.Insert +import androidx.room.MapColumn import androidx.room.Query import androidx.room.RawQuery import androidx.room.Update @@ -14,6 +15,8 @@ import io.github.zyrouge.symphony.services.groove.entities.Artist import io.github.zyrouge.symphony.services.groove.entities.ArtistSongMapping import io.github.zyrouge.symphony.services.groove.entities.Composer import io.github.zyrouge.symphony.services.groove.entities.ComposerSongMapping +import io.github.zyrouge.symphony.services.groove.entities.GenreSongMapping +import io.github.zyrouge.symphony.services.groove.entities.PlaylistSongMapping import io.github.zyrouge.symphony.services.groove.entities.Song import io.github.zyrouge.symphony.services.groove.repositories.SongRepository import kotlinx.coroutines.flow.Flow @@ -40,31 +43,55 @@ interface SongStore { @RawQuery( observedEntities = [ + AlbumSongMapping::class, + ArtistSongMapping::class, + ComposerSongMapping::class, + GenreSongMapping::class, + PlaylistSongMapping::class, Song::class, - Artist::class, + ] + ) + fun entriesAsFlowRaw(query: SupportSQLiteQuery): Flow> + + @RawQuery( + observedEntities = [ + AlbumSongMapping::class, ArtistSongMapping::class, - Album::class, + ComposerSongMapping::class, + GenreSongMapping::class, + PlaylistSongMapping::class, + Song::class, + ] + ) + fun entriesAsPlaylistSongMappedFlowRaw(query: SupportSQLiteQuery): Flow> + + @RawQuery( + observedEntities = [ AlbumSongMapping::class, - AlbumArtistMapping::class, - Composer::class, + ArtistSongMapping::class, ComposerSongMapping::class, + GenreSongMapping::class, + PlaylistSongMapping::class, + Song::class, ] ) - fun valuesAsFlowRaw(query: SupportSQLiteQuery): Flow> + fun valuesAsFlowRaw(query: SupportSQLiteQuery): Flow> } -fun SongStore.valuesAsFlow( +internal fun SongStore.valuesAsFlowQuery( sortBy: SongRepository.SortBy, sortReverse: Boolean, + additionalClauseAfterSongSelect: String = "", additionalClauseBeforeJoins: String = "", additionalArgsBeforeJoins: Array = emptyArray(), -): Flow> { + overrideOrderBy: String? = null, +): SupportSQLiteQuery { val aliasFirstAlbumArtist = "firstAlbumArtist" val embeddedFirstArtistName = "firstArtistName" val embeddedFirstAlbumName = "firstAlbumName" val embeddedFirstAlbumArtistName = "firstAlbumArtistName" val embeddedFirstComposerName = "firstComposerName" - val orderBy = when (sortBy) { + val orderBy = overrideOrderBy ?: when (sortBy) { SongRepository.SortBy.CUSTOM -> "${Song.TABLE}.${Song.COLUMN_ID}" SongRepository.SortBy.TITLE -> "${Song.TABLE}.${Song.COLUMN_ID}" SongRepository.SortBy.ARTIST -> embeddedFirstArtistName @@ -95,6 +122,7 @@ fun SongStore.valuesAsFlow( "FROM ${ComposerSongMapping.TABLE} " + "WHERE ${ComposerSongMapping.TABLE}.${ComposerSongMapping.COLUMN_SONG_ID} = ${Song.COLUMN_ID}" val query = "SELECT ${Song.TABLE}.*, " + + additionalClauseAfterSongSelect + "${Artist.TABLE}.${Artist.COLUMN_NAME} as $embeddedFirstArtistName, " + "${Album.TABLE}.${Album.COLUMN_NAME} as $embeddedFirstAlbumName, " + "$aliasFirstAlbumArtist.${Artist.COLUMN_NAME} as $embeddedFirstAlbumArtistName, " + @@ -107,5 +135,24 @@ fun SongStore.valuesAsFlow( "LEFT JOIN ${Composer.TABLE} ON ${Composer.TABLE}.${Composer.COLUMN_ID} = ($composerQuery)" + "ORDER BY $orderBy $orderDirection" val args = additionalArgsBeforeJoins - return valuesAsFlowRaw(SimpleSQLiteQuery(query, args)) + return SimpleSQLiteQuery(query, args) +} + +fun SongStore.valuesAsFlow( + sortBy: SongRepository.SortBy, + sortReverse: Boolean, + additionalClauseAfterSongSelect: String = "", + additionalClauseBeforeJoins: String = "", + additionalArgsBeforeJoins: Array = emptyArray(), + overrideOrderBy: String? = null, +): Flow> { + val query = valuesAsFlowQuery( + sortBy = sortBy, + sortReverse = sortReverse, + additionalClauseAfterSongSelect = additionalClauseAfterSongSelect, + additionalClauseBeforeJoins = additionalClauseBeforeJoins, + additionalArgsBeforeJoins = additionalArgsBeforeJoins, + overrideOrderBy = overrideOrderBy, + ) + return valuesAsFlowRaw(query) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/MediaExposer.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/MediaExposer.kt index 6e31cbb6..8f47365d 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/MediaExposer.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/MediaExposer.kt @@ -119,8 +119,8 @@ class MediaExposer(private val symphony: Symphony) { val songFileStaleIds: ConcurrentSet, val lyricFileStaleIds: ConcurrentSet, val songStaleIds: ConcurrentSet, - val artworkIndexCache: ConcurrentHashMap, - val artworkStaleFiles: ConcurrentSet, + val songArtworkIndexCache: ConcurrentHashMap, + val songArtworkStaleFiles: ConcurrentSet, val filter: MediaFilter, val songParseOptions: MediaTreeSongFile.ParseOptions, ) { @@ -204,13 +204,13 @@ class MediaExposer(private val symphony: Symphony) { exFile: MediaTreeSongFile?, document: DocumentFileX, ) { - val exArtworkIndex = artworkIndexCache[exFile?.id] + val exArtworkIndex = songArtworkIndexCache[exFile?.id] val skipArtworkParsing = exArtworkIndex != null && exArtworkIndex.let { - exArtworkIndex.file == null || artworkStaleFiles.contains(exArtworkIndex.file) + exArtworkIndex.file == null || songArtworkStaleFiles.contains(exArtworkIndex.file) } if (skipArtworkParsing && exFile?.dateModified == document.lastModified) { songFileStaleIds.remove(exFile.id) - exArtworkIndex.file?.let { artworkStaleFiles.remove(it) } + exArtworkIndex.file?.let { songArtworkStaleFiles.remove(it) } return } val id = exFile?.id ?: symphony.database.mediaTreeSongFilesIdGenerator.next() @@ -231,12 +231,12 @@ class MediaExposer(private val symphony: Symphony) { val quality = symphony.settings.artworkQuality.value if (quality.maxSide == null && extension != "_") { val name = "$artworkId.$extension" - symphony.database.artworks.get(name).writeBytes(it.data) + symphony.database.songArtworks.get(name).writeBytes(it.data) return@let name } val bitmap = BitmapFactory.decodeByteArray(it.data, 0, it.data.size) val name = "$artworkId.jpg" - FileOutputStream(symphony.database.artworks.get(name)).use { writer -> + FileOutputStream(symphony.database.songArtworks.get(name)).use { writer -> ImagePreserver .resize(bitmap, quality) .compress(Bitmap.CompressFormat.JPEG, 100, writer) @@ -245,11 +245,11 @@ class MediaExposer(private val symphony: Symphony) { } val artworkIndex = SongArtworkIndex(songId = id, file = artworkFile) artworkIndex.file?.let { - artworkStaleFiles.remove(it) + songArtworkStaleFiles.remove(it) } symphony.database.mediaTreeSongFiles.update(file) if (exArtworkIndex?.file != artworkIndex.file) { - symphony.database.artworkIndices.upsert(artworkIndex) + symphony.database.songArtworkIndices.upsert(artworkIndex) } extended.lyrics?.let { symphony.database.songLyrics.upsert(SongLyric(id, it)) @@ -436,9 +436,9 @@ class MediaExposer(private val symphony: Symphony) { } catch (err: Exception) { Logger.warn("MediaExposer", "trimming song files failed", err) } - for (x in artworkStaleFiles) { + for (x in songArtworkStaleFiles) { try { - symphony.database.artworks.get(x).delete() + symphony.database.songArtworks.get(x).delete() } catch (err: Exception) { Logger.warn("MediaExposer", "deleting artwork failed", err) } @@ -464,8 +464,8 @@ class MediaExposer(private val symphony: Symphony) { songFileStaleIds = concurrentSetOf(songFileIds), lyricFileStaleIds = concurrentSetOf(lyricFileIds), songStaleIds = concurrentSetOf(songIds), - artworkIndexCache = ConcurrentHashMap(symphony.database.artworkIndices.entriesSongIdMapped()), - artworkStaleFiles = concurrentSetOf(symphony.database.artworks.all()), + songArtworkIndexCache = ConcurrentHashMap(symphony.database.songArtworkIndices.entriesSongIdMapped()), + songArtworkStaleFiles = concurrentSetOf(symphony.database.songArtworks.all()), filter = filter, songParseOptions = MediaTreeSongFile.ParseOptions.create(symphony), ) diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Song.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Song.kt index 35b01071..33aebfe1 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Song.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Song.kt @@ -67,9 +67,18 @@ data class Song( @ColumnInfo(COLUMN_PATH) val path: String, ) { - data class AlongAttribute( + data class AlongSongQueueMapping( @Embedded val song: Song, + @Embedded + val mapping: SongQueueSongMapping, + ) + + data class AlongPlaylistMapping( + @Embedded + val song: Song, + @Embedded + val mapping: PlaylistSongMapping, ) val bitrateK: Long? get() = bitrate?.let { it / 1000 } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongArtworkIndex.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongArtworkIndex.kt index a8a2c81d..5aa3865d 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongArtworkIndex.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongArtworkIndex.kt @@ -18,7 +18,7 @@ import androidx.room.PrimaryKey onDelete = ForeignKey.CASCADE, ), ], - indices = [Index(SongArtworkIndex.COLUMN_FILE)], + indices = [Index(SongArtworkIndex.COLUMN_SONG_ID)], ) data class SongArtworkIndex( @PrimaryKey @@ -28,7 +28,8 @@ data class SongArtworkIndex( val file: String?, ) { companion object { - const val TABLE = "song_artwork_indices" + const val TABLE = "song_artwork" + const val COLUMN_ID = "id" const val COLUMN_SONG_ID = "song_id" const val COLUMN_FILE = "file" } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueue.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueue.kt new file mode 100644 index 00000000..a7d27c59 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueue.kt @@ -0,0 +1,50 @@ +package io.github.zyrouge.symphony.services.groove.entities + +import androidx.compose.runtime.Immutable +import androidx.room.ColumnInfo +import androidx.room.Embedded +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Immutable +@Entity(SongQueue.TABLE) +data class SongQueue( + @PrimaryKey + @ColumnInfo(COLUMN_ID) + val id: String, + @ColumnInfo(COLUMN_PLAYING_ID) + val playingId: String, + @ColumnInfo(COLUMN_SHUFFLED) + val shuffled: Boolean, + @ColumnInfo(COLUMN_LOOP_MODE) + val loopMode: LoopMode, +) { + enum class LoopMode { + None, + Queue, + Song; + + companion object { + val values = enumValues() + } + } + + data class AlongAttributes( + @Embedded + val queue: SongQueue, + @Embedded + val tracksCount: Int, + ) { + companion object { + const val EMBEDDED_TRACKS_COUNT = "tracksCount" + } + } + + companion object { + const val TABLE = "song_queue" + const val COLUMN_ID = "id" + const val COLUMN_PLAYING_ID = "playing_id" + const val COLUMN_SHUFFLED = "shuffled" + const val COLUMN_LOOP_MODE = "loop_mode" + } +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueueSongMapping.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueueSongMapping.kt new file mode 100644 index 00000000..2825c0bb --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueueSongMapping.kt @@ -0,0 +1,60 @@ +package io.github.zyrouge.symphony.services.groove.entities + +import androidx.compose.runtime.Immutable +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey + +@Immutable +@Entity( + SongQueueSongMapping.TABLE, + foreignKeys = [ + ForeignKey( + entity = SongQueue::class, + parentColumns = arrayOf(SongQueue.COLUMN_ID), + childColumns = arrayOf(SongQueueSongMapping.COLUMN_QUEUE_ID), + onDelete = ForeignKey.CASCADE, + ), + ForeignKey( + entity = Song::class, + parentColumns = arrayOf(Song.COLUMN_ID), + childColumns = arrayOf(SongQueueSongMapping.COLUMN_SONG_ID), + onDelete = ForeignKey.SET_NULL, + ), + ForeignKey( + entity = SongQueueSongMapping::class, + parentColumns = arrayOf(SongQueueSongMapping.COLUMN_ID), + childColumns = arrayOf(SongQueueSongMapping.COLUMN_NEXT_ID), + onDelete = ForeignKey.SET_NULL, + ), + ], + indices = [ + Index(SongQueueSongMapping.COLUMN_QUEUE_ID), + Index(SongQueueSongMapping.COLUMN_IS_HEAD), + Index(SongQueueSongMapping.COLUMN_NEXT_ID), + ], +) +data class SongQueueSongMapping( + @PrimaryKey + @ColumnInfo(COLUMN_ID) + val id: String, + @ColumnInfo(COLUMN_QUEUE_ID) + val queueId: String, + @ColumnInfo(COLUMN_SONG_ID) + val songId: String?, + @ColumnInfo(COLUMN_IS_HEAD) + val isStart: Boolean, + @ColumnInfo(COLUMN_NEXT_ID) + val nextId: String?, +) { + companion object { + const val TABLE = "song_queue_songs_mapping" + const val COLUMN_ID = "id" + const val COLUMN_QUEUE_ID = "queue_id" + const val COLUMN_SONG_ID = "song_id" + const val COLUMN_IS_HEAD = "is_head" + const val COLUMN_NEXT_ID = "next_id" + } +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/SongRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/SongRepository.kt index 051ef6d0..6cb40f27 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/SongRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/SongRepository.kt @@ -1,7 +1,10 @@ package io.github.zyrouge.symphony.services.groove.repositories +import androidx.core.net.toUri import io.github.zyrouge.symphony.Symphony import io.github.zyrouge.symphony.services.database.store.valuesAsFlow +import io.github.zyrouge.symphony.ui.helpers.Assets +import io.github.zyrouge.symphony.ui.helpers.createHandyImageRequest class SongRepository(private val symphony: Symphony) { enum class SortBy { @@ -18,6 +21,17 @@ class SongRepository(private val symphony: Symphony) { TRACK_NUMBER, } + fun createArtworkImageRequest(songId: String) = createHandyImageRequest( + symphony.applicationContext, + image = getArtworkUri(songId), + fallback = Assets.getPlaceholderId(symphony), + ) + + fun getArtworkUri(songId: String) = + symphony.database.songArtworkIndices.findBySongId(songId)?.file + ?.let { symphony.database.songArtworks.get(it).toUri() } + ?: Assets.getPlaceholderUri(symphony) + fun valuesAsFlow(sortBy: SortBy, sortReverse: Boolean) = symphony.database.songs.valuesAsFlow(sortBy, sortReverse) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Album.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Album.kt index f96e1fea..85090226 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Album.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Album.kt @@ -69,7 +69,7 @@ fun AlbumView(context: ViewContext, route: AlbumViewRoute) { album.id, songsSortBy, songsSortReverse, - ).transformLatest { emit(it.map { x -> x.song }) } + ) } emitAll(value) } @@ -136,7 +136,7 @@ fun AlbumView(context: ViewContext, route: AlbumViewRoute) { @OptIn(ExperimentalLayoutApi::class) @Composable -private fun AlbumHero(context: ViewContext, album: Album) { +private fun AlbumHero(context: ViewContext, album: Album.AlongAttributes) { GenericGrooveBanner( image = album.createArtworkImageRequest(context.symphony).build(), options = { expanded, onDismissRequest -> diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/AlbumArtists.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/AlbumArtists.kt index 3dca8f47..7e53b503 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/AlbumArtists.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/AlbumArtists.kt @@ -7,7 +7,7 @@ import io.github.zyrouge.symphony.ui.components.AlbumArtistGrid import io.github.zyrouge.symphony.ui.components.LoaderScaffold import io.github.zyrouge.symphony.ui.helpers.ViewContext import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.transformLatest +import kotlinx.coroutines.flow.mapLatest @OptIn(ExperimentalCoroutinesApi::class) @Composable @@ -16,7 +16,7 @@ fun AlbumArtistsView(context: ViewContext) { val sortBy by context.symphony.settings.lastUsedAlbumArtistsSortBy.flow.collectAsState() val sortReverse by context.symphony.settings.lastUsedAlbumArtistsSortReverse.flow.collectAsState() val albumArtists by context.symphony.groove.albumArtist.valuesAsFlow(sortBy, sortReverse) - .transformLatest { emit(it.map { x -> x.artist }) } + .mapLatest { it.map { x -> x.artist } } .collectAsState(emptyList()) LoaderScaffold(context, isLoading = isUpdating) { diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Albums.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Albums.kt index c3acb2a7..7dbb31fa 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Albums.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Albums.kt @@ -7,7 +7,7 @@ import io.github.zyrouge.symphony.ui.components.AlbumGrid import io.github.zyrouge.symphony.ui.components.LoaderScaffold import io.github.zyrouge.symphony.ui.helpers.ViewContext import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.transformLatest +import kotlinx.coroutines.flow.mapLatest @OptIn(ExperimentalCoroutinesApi::class) @Composable @@ -16,7 +16,7 @@ fun AlbumsView(context: ViewContext) { val sortBy by context.symphony.settings.lastUsedAlbumsSortBy.flow.collectAsState() val sortReverse by context.symphony.settings.lastUsedAlbumsSortReverse.flow.collectAsState() val albums by context.symphony.groove.album.valuesAsFlow(sortBy, sortReverse) - .transformLatest { emit(it.map { x -> x.album }) } + .mapLatest { it.map { x -> x.album } } .collectAsState(emptyList()) LoaderScaffold(context, isLoading = isUpdating) { diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Artists.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Artists.kt index f37aab90..4b80e229 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Artists.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Artists.kt @@ -7,7 +7,7 @@ import io.github.zyrouge.symphony.ui.components.ArtistGrid import io.github.zyrouge.symphony.ui.components.LoaderScaffold import io.github.zyrouge.symphony.ui.helpers.ViewContext import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.transformLatest +import kotlinx.coroutines.flow.mapLatest @OptIn(ExperimentalCoroutinesApi::class) @Composable @@ -16,7 +16,7 @@ fun ArtistsView(context: ViewContext) { val sortBy by context.symphony.settings.lastUsedArtistsSortBy.flow.collectAsState() val sortReverse by context.symphony.settings.lastUsedArtistsSortReverse.flow.collectAsState() val artists by context.symphony.groove.artist.valuesAsFlow(sortBy, sortReverse) - .transformLatest { emit(it.map { x -> x.artist }) } + .mapLatest { it.map { x -> x.artist } } .collectAsState(emptyList()) LoaderScaffold(context, isLoading = isUpdating) { diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Browser.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Browser.kt index aa934527..f1ffe09f 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Browser.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Browser.kt @@ -10,8 +10,7 @@ import io.github.zyrouge.symphony.utils.SimplePath @Composable fun BrowserView(context: ViewContext) { - val isUpdating by context.symphony.groove.song.isUpdating.collectAsState() - val id by context.symphony.groove.song.id.collectAsState() + val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsState() val explorer = context.symphony.groove.song.explorer val lastUsedFolderPath by context.symphony.settings.lastUsedBrowserPath.flow.collectAsState() diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Playlists.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Playlists.kt index 6211970f..5090471e 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Playlists.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Playlists.kt @@ -34,7 +34,7 @@ import io.github.zyrouge.symphony.ui.helpers.ViewContext import io.github.zyrouge.symphony.utils.ActivityUtils import io.github.zyrouge.symphony.utils.Logger import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.transformLatest +import kotlinx.coroutines.flow.mapLatest @OptIn(ExperimentalCoroutinesApi::class) @Composable @@ -43,7 +43,7 @@ fun PlaylistsView(context: ViewContext) { val sortBy by context.symphony.settings.lastUsedPlaylistsSortBy.flow.collectAsState() val sortReverse by context.symphony.settings.lastUsedPlaylistsSortReverse.flow.collectAsState() val playlists by context.symphony.groove.playlist.valuesAsFlow(sortBy, sortReverse) - .transformLatest { emit(it.map { x -> x.playlist }) } + .mapLatest { it.map { x -> x.playlist } } .collectAsState(emptyList()) var showPlaylistCreator by remember { mutableStateOf(false) } From 86ccef81ba57da0f8f703df4f355f25b3af0d638 Mon Sep 17 00:00:00 2001 From: Zyrouge Date: Wed, 12 Feb 2025 01:26:31 +0530 Subject: [PATCH 06/15] refactor: artwork queries --- .../database/store/AlbumSongMappingStore.kt | 18 ++++++++++++++++ .../database/store/ArtistSongMappingStore.kt | 18 ++++++++++++++++ .../store/ComposerSongMappingStore.kt | 18 ++++++++++++++++ .../store/PlaylistSongMappingStore.kt | 19 ++++++++++++++++- .../database/store/SongArtworkIndexStore.kt | 3 ++- .../store/SongQueueSongMappingStore.kt | 4 ++-- .../symphony/services/groove/MediaExposer.kt | 2 +- .../groove/entities/PlaylistSongMapping.kt | 2 +- .../symphony/services/groove/entities/Song.kt | 3 --- .../services/groove/entities/SongQueue.kt | 18 ++++++++++++++-- .../groove/entities/SongQueueSongMapping.kt | 2 +- .../groove/repositories/AlbumRepository.kt | 10 +++++++++ .../groove/repositories/ArtistRepository.kt | 10 +++++++++ .../groove/repositories/ComposerRepository.kt | 10 +++++++++ .../groove/repositories/PlaylistRepository.kt | 10 +++++++++ .../groove/repositories/SongRepository.kt | 21 +++++++++---------- .../zyrouge/symphony/ui/helpers/Assets.kt | 6 ++++-- 17 files changed, 149 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumSongMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumSongMappingStore.kt index dcc4e1ab..831c0e4b 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumSongMappingStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumSongMappingStore.kt @@ -3,13 +3,20 @@ package io.github.zyrouge.symphony.services.database.store import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy +import androidx.room.RawQuery +import androidx.sqlite.db.SimpleSQLiteQuery import io.github.zyrouge.symphony.services.groove.entities.AlbumSongMapping +import io.github.zyrouge.symphony.services.groove.entities.SongArtworkIndex import io.github.zyrouge.symphony.services.groove.repositories.SongRepository +import kotlinx.coroutines.flow.Flow @Dao interface AlbumSongMappingStore { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun upsert(vararg entities: AlbumSongMapping) + + @RawQuery(observedEntities = [SongArtworkIndex::class, AlbumSongMapping::class]) + fun findTop4SongArtworksAsFlowRaw(query: SimpleSQLiteQuery): Flow> } fun AlbumSongMappingStore.valuesMappedAsFlow( @@ -23,3 +30,14 @@ fun AlbumSongMappingStore.valuesMappedAsFlow( additionalClauseBeforeJoins = "JOIN ${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_ALBUM_ID} = ? ", additionalArgsBeforeJoins = arrayOf(id), ) + +fun AlbumSongMappingStore.findTop4SongArtworksAsFlow(albumId: String): Flow> { + val query = "SELECT ${SongArtworkIndex.TABLE}.* " + + "FROM ${SongArtworkIndex.TABLE} " + + "LEFT JOIN ${AlbumSongMapping.TABLE} ON ${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_ALBUM_ID} = ? AND ${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_SONG_ID} = ${SongArtworkIndex.TABLE}.${SongArtworkIndex.COLUMN_SONG_ID} " + + "WHERE ${SongArtworkIndex.TABLE}.${SongArtworkIndex.COLUMN_FILE} != null " + + "ORDER BY DESC " + + "LIMIT 4" + val args = arrayOf(albumId) + return findTop4SongArtworksAsFlowRaw(SimpleSQLiteQuery(query, args)) +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistSongMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistSongMappingStore.kt index a699becc..a70bbaf3 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistSongMappingStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistSongMappingStore.kt @@ -3,13 +3,20 @@ package io.github.zyrouge.symphony.services.database.store import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy +import androidx.room.RawQuery +import androidx.sqlite.db.SimpleSQLiteQuery import io.github.zyrouge.symphony.services.groove.entities.ArtistSongMapping +import io.github.zyrouge.symphony.services.groove.entities.SongArtworkIndex import io.github.zyrouge.symphony.services.groove.repositories.SongRepository +import kotlinx.coroutines.flow.Flow @Dao interface ArtistSongMappingStore { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun upsert(vararg entities: ArtistSongMapping) + + @RawQuery(observedEntities = [SongArtworkIndex::class, ArtistSongMapping::class]) + fun findTop4SongArtworksAsFlowRaw(query: SimpleSQLiteQuery): Flow> } fun ArtistSongMappingStore.valuesMappedAsFlow( @@ -23,3 +30,14 @@ fun ArtistSongMappingStore.valuesMappedAsFlow( additionalClauseBeforeJoins = "JOIN ${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_ARTIST_ID} = ? ", additionalArgsBeforeJoins = arrayOf(id), ) + +fun ArtistSongMappingStore.findTop4SongArtworksAsFlow(artistId: String): Flow> { + val query = "SELECT ${SongArtworkIndex.TABLE}.* " + + "FROM ${SongArtworkIndex.TABLE} " + + "LEFT JOIN ${ArtistSongMapping.TABLE} ON ${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_ARTIST_ID} = ? AND ${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_SONG_ID} = ${SongArtworkIndex.TABLE}.${SongArtworkIndex.COLUMN_SONG_ID} " + + "WHERE ${SongArtworkIndex.TABLE}.${SongArtworkIndex.COLUMN_FILE} != null " + + "ORDER BY DESC " + + "LIMIT 4" + val args = arrayOf(artistId) + return findTop4SongArtworksAsFlowRaw(SimpleSQLiteQuery(query, args)) +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ComposerSongMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ComposerSongMappingStore.kt index 5196de53..080174ee 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ComposerSongMappingStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ComposerSongMappingStore.kt @@ -3,13 +3,20 @@ package io.github.zyrouge.symphony.services.database.store import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy +import androidx.room.RawQuery +import androidx.sqlite.db.SimpleSQLiteQuery import io.github.zyrouge.symphony.services.groove.entities.ComposerSongMapping +import io.github.zyrouge.symphony.services.groove.entities.SongArtworkIndex import io.github.zyrouge.symphony.services.groove.repositories.SongRepository +import kotlinx.coroutines.flow.Flow @Dao interface ComposerSongMappingStore { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun upsert(vararg entities: ComposerSongMapping) + + @RawQuery(observedEntities = [SongArtworkIndex::class, ComposerSongMapping::class]) + fun findTop4SongArtworksAsFlowRaw(query: SimpleSQLiteQuery): Flow> } fun ComposerSongMappingStore.valuesMappedAsFlow( @@ -23,3 +30,14 @@ fun ComposerSongMappingStore.valuesMappedAsFlow( additionalClauseBeforeJoins = "JOIN ${ComposerSongMapping.TABLE}.${ComposerSongMapping.COLUMN_COMPOSER_ID} = ? ", additionalArgsBeforeJoins = arrayOf(id), ) + +fun ComposerSongMappingStore.findTop4SongArtworksAsFlow(composerId: String): Flow> { + val query = "SELECT ${SongArtworkIndex.TABLE}.* " + + "FROM ${SongArtworkIndex.TABLE} " + + "LEFT JOIN ${ComposerSongMapping.TABLE} ON ${ComposerSongMapping.TABLE}.${ComposerSongMapping.COLUMN_COMPOSER_ID} = ? AND ${ComposerSongMapping.TABLE}.${ComposerSongMapping.COLUMN_SONG_ID} = ${SongArtworkIndex.TABLE}.${SongArtworkIndex.COLUMN_SONG_ID} " + + "WHERE ${SongArtworkIndex.TABLE}.${SongArtworkIndex.COLUMN_FILE} != null " + + "ORDER BY DESC " + + "LIMIT 4" + val args = arrayOf(composerId) + return findTop4SongArtworksAsFlowRaw(SimpleSQLiteQuery(query, args)) +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistSongMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistSongMappingStore.kt index 4f8a5479..cb62f999 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistSongMappingStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistSongMappingStore.kt @@ -3,8 +3,11 @@ package io.github.zyrouge.symphony.services.database.store import androidx.room.Dao import androidx.room.Insert import androidx.room.Query +import androidx.room.RawQuery +import androidx.sqlite.db.SimpleSQLiteQuery import io.github.zyrouge.symphony.services.groove.entities.PlaylistSongMapping import io.github.zyrouge.symphony.services.groove.entities.Song +import io.github.zyrouge.symphony.services.groove.entities.SongArtworkIndex import io.github.zyrouge.symphony.services.groove.repositories.SongRepository import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow @@ -17,6 +20,9 @@ interface PlaylistSongMappingStore { @Query("DELETE FROM ${PlaylistSongMapping.TABLE} WHERE ${PlaylistSongMapping.COLUMN_PLAYLIST_ID} IN (:ids)") suspend fun deletePlaylistIds(ids: Collection) + + @RawQuery(observedEntities = [SongArtworkIndex::class, PlaylistSongMapping::class]) + fun findTop4SongArtworksAsFlowRaw(query: SimpleSQLiteQuery): Flow> } @OptIn(ExperimentalCoroutinesApi::class) @@ -37,7 +43,7 @@ fun PlaylistSongMappingStore.valuesMappedAsFlow( val list = mutableListOf() var head = it.firstNotNullOfOrNull { when { - it.value.mapping.isStart -> it.value + it.value.mapping.isHead -> it.value else -> null } } @@ -48,3 +54,14 @@ fun PlaylistSongMappingStore.valuesMappedAsFlow( list.toList() } } + +fun PlaylistSongMappingStore.findTop4SongArtworksAsFlow(playlistId: String): Flow> { + val query = "SELECT ${SongArtworkIndex.TABLE}.* " + + "FROM ${SongArtworkIndex.TABLE} " + + "LEFT JOIN ${PlaylistSongMapping.TABLE} ON ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_PLAYLIST_ID} = ? AND ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_SONG_ID} = ${SongArtworkIndex.TABLE}.${SongArtworkIndex.COLUMN_SONG_ID} " + + "WHERE ${SongArtworkIndex.TABLE}.${SongArtworkIndex.COLUMN_FILE} != null " + + "ORDER BY DESC " + + "LIMIT 4" + val args = arrayOf(playlistId) + return findTop4SongArtworksAsFlowRaw(SimpleSQLiteQuery(query, args)) +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongArtworkIndexStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongArtworkIndexStore.kt index 37dd1f17..22ca3da3 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongArtworkIndexStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongArtworkIndexStore.kt @@ -6,6 +6,7 @@ import androidx.room.MapColumn import androidx.room.OnConflictStrategy import androidx.room.Query import io.github.zyrouge.symphony.services.groove.entities.SongArtworkIndex +import kotlinx.coroutines.flow.Flow @Dao interface SongArtworkIndexStore { @@ -13,7 +14,7 @@ interface SongArtworkIndexStore { suspend fun upsert(vararg entities: SongArtworkIndex): List @Query("SELECT * FROM ${SongArtworkIndex.TABLE} WHERE ${SongArtworkIndex.COLUMN_SONG_ID} = :songId LIMIT 1") - fun findBySongId(songId: String): SongArtworkIndex? + fun findBySongIdAsFlow(songId: String): Flow @Query("SELECT * FROM ${SongArtworkIndex.TABLE} WHERE ${SongArtworkIndex.COLUMN_SONG_ID} != null") fun entriesSongIdMapped(): Map<@MapColumn(SongArtworkIndex.COLUMN_SONG_ID) String, SongArtworkIndex> diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueSongMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueSongMappingStore.kt index dc3bde3f..61396573 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueSongMappingStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueSongMappingStore.kt @@ -30,7 +30,7 @@ fun SongQueueSongMappingStore.entriesAsFlow(queueId: String): Flow() var head = it.firstNotNullOfOrNull { when { - it.value.mapping.isStart -> it.value + it.value.mapping.isHead -> it.value else -> null } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/MediaExposer.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/MediaExposer.kt index 8f47365d..ed0680b1 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/MediaExposer.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/MediaExposer.kt @@ -97,7 +97,7 @@ class MediaExposer(private val symphony: Symphony) { playlistId = playlistId, songId = null, songPath = x, - isStart = i == 0, + isHead = i == 0, nextId = nextPlaylistSongMapping?.id, ) playlistSongMappingToBeInserted.add(playlistSongMapping) diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/PlaylistSongMapping.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/PlaylistSongMapping.kt index 392a34fe..9d2f2f7d 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/PlaylistSongMapping.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/PlaylistSongMapping.kt @@ -53,7 +53,7 @@ data class PlaylistSongMapping( @ColumnInfo(COLUMN_SONG_PATH) val songPath: String?, @ColumnInfo(COLUMN_IS_HEAD) - val isStart: Boolean, + val isHead: Boolean, @ColumnInfo(COLUMN_NEXT_ID) val nextId: String?, ) { diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Song.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Song.kt index 33aebfe1..ab9b9b2f 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Song.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Song.kt @@ -90,9 +90,6 @@ data class Song( .toFloat() } - fun createArtworkImageRequest(symphony: Symphony) = - symphony.groove.song.createArtworkImageRequest(id) - fun toSamplingInfoString(symphony: Symphony): String? { val values = mutableListOf() encoder?.let { diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueue.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueue.kt index a7d27c59..96ae3ce7 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueue.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueue.kt @@ -4,16 +4,29 @@ import androidx.compose.runtime.Immutable import androidx.room.ColumnInfo import androidx.room.Embedded import androidx.room.Entity +import androidx.room.ForeignKey import androidx.room.PrimaryKey @Immutable -@Entity(SongQueue.TABLE) +@Entity( + SongQueue.TABLE, + foreignKeys = [ + ForeignKey( + entity = SongQueueSongMapping::class, + parentColumns = arrayOf(SongQueueSongMapping.COLUMN_ID), + childColumns = arrayOf(SongQueue.COLUMN_PLAYING_ID), + onDelete = ForeignKey.SET_NULL, + ), + ], +) data class SongQueue( @PrimaryKey @ColumnInfo(COLUMN_ID) val id: String, @ColumnInfo(COLUMN_PLAYING_ID) - val playingId: String, + val playingId: String?, + @ColumnInfo(COLUMN_PLAYING_TIMESTAMP) + val playingTimestamp: Long?, @ColumnInfo(COLUMN_SHUFFLED) val shuffled: Boolean, @ColumnInfo(COLUMN_LOOP_MODE) @@ -44,6 +57,7 @@ data class SongQueue( const val TABLE = "song_queue" const val COLUMN_ID = "id" const val COLUMN_PLAYING_ID = "playing_id" + const val COLUMN_PLAYING_TIMESTAMP = "playing_timestamp" const val COLUMN_SHUFFLED = "shuffled" const val COLUMN_LOOP_MODE = "loop_mode" } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueueSongMapping.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueueSongMapping.kt index 2825c0bb..60e0922a 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueueSongMapping.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueueSongMapping.kt @@ -45,7 +45,7 @@ data class SongQueueSongMapping( @ColumnInfo(COLUMN_SONG_ID) val songId: String?, @ColumnInfo(COLUMN_IS_HEAD) - val isStart: Boolean, + val isHead: Boolean, @ColumnInfo(COLUMN_NEXT_ID) val nextId: String?, ) { diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumRepository.kt index 443eadfe..6e440713 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumRepository.kt @@ -1,8 +1,11 @@ package io.github.zyrouge.symphony.services.groove.repositories import io.github.zyrouge.symphony.Symphony +import io.github.zyrouge.symphony.services.database.store.findTop4SongArtworksAsFlow import io.github.zyrouge.symphony.services.database.store.valuesAsFlow import io.github.zyrouge.symphony.services.database.store.valuesMappedAsFlow +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.mapLatest class AlbumRepository(private val symphony: Symphony) { enum class SortBy { @@ -24,6 +27,13 @@ class AlbumRepository(private val symphony: Symphony) { sortReverse ) + @OptIn(ExperimentalCoroutinesApi::class) + fun getTop4ArtworkUriAsFlow(id: String) = + symphony.database.albumSongMapping.findTop4SongArtworksAsFlow(id) + .mapLatest { indices -> + indices.map { symphony.groove.song.getArtworkUriFromIndex(it) } + } + fun valuesAsFlow(sortBy: SortBy, sortReverse: Boolean) = symphony.database.albums.valuesAsFlow(sortBy, sortReverse) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/ArtistRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/ArtistRepository.kt index 8b223612..527a2f96 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/ArtistRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/ArtistRepository.kt @@ -1,7 +1,10 @@ package io.github.zyrouge.symphony.services.groove.repositories import io.github.zyrouge.symphony.Symphony +import io.github.zyrouge.symphony.services.database.store.findTop4SongArtworksAsFlow import io.github.zyrouge.symphony.services.database.store.valuesAsFlow +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.mapLatest class ArtistRepository(private val symphony: Symphony) { enum class SortBy { @@ -11,6 +14,13 @@ class ArtistRepository(private val symphony: Symphony) { ALBUMS_COUNT, } + @OptIn(ExperimentalCoroutinesApi::class) + fun getTop4ArtworkUriAsFlow(id: String) = + symphony.database.artistSongMapping.findTop4SongArtworksAsFlow(id) + .mapLatest { indices -> + indices.map { symphony.groove.song.getArtworkUriFromIndex(it) } + } + fun valuesAsFlow(sortBy: SortBy, sortReverse: Boolean) = symphony.database.artists.valuesAsFlow(sortBy, sortReverse) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/ComposerRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/ComposerRepository.kt index 21a92995..32bec624 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/ComposerRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/ComposerRepository.kt @@ -1,7 +1,10 @@ package io.github.zyrouge.symphony.services.groove.repositories import io.github.zyrouge.symphony.Symphony +import io.github.zyrouge.symphony.services.database.store.findTop4SongArtworksAsFlow import io.github.zyrouge.symphony.services.database.store.valuesAsFlow +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.mapLatest class ComposerRepository(private val symphony: Symphony) { enum class SortBy { @@ -11,6 +14,13 @@ class ComposerRepository(private val symphony: Symphony) { ALBUMS_COUNT, } + @OptIn(ExperimentalCoroutinesApi::class) + fun getTop4ArtworkUriAsFlow(id: String) = + symphony.database.composerSongMapping.findTop4SongArtworksAsFlow(id) + .mapLatest { indices -> + indices.map { symphony.groove.song.getArtworkUriFromIndex(it) } + } + fun valuesAsFlow(sortBy: SortBy, sortReverse: Boolean) = symphony.database.composers.valuesAsFlow(sortBy, sortReverse) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/PlaylistRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/PlaylistRepository.kt index 93995ddd..76bc8ada 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/PlaylistRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/PlaylistRepository.kt @@ -1,7 +1,10 @@ package io.github.zyrouge.symphony.services.groove.repositories import io.github.zyrouge.symphony.Symphony +import io.github.zyrouge.symphony.services.database.store.findTop4SongArtworksAsFlow import io.github.zyrouge.symphony.services.database.store.valuesAsFlow +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.mapLatest class PlaylistRepository(private val symphony: Symphony) { enum class SortBy { @@ -10,6 +13,13 @@ class PlaylistRepository(private val symphony: Symphony) { TRACKS_COUNT, } + @OptIn(ExperimentalCoroutinesApi::class) + fun getTop4ArtworkUriAsFlow(id: String) = + symphony.database.playlistSongMapping.findTop4SongArtworksAsFlow(id) + .mapLatest { indices -> + indices.map { symphony.groove.song.getArtworkUriFromIndex(it) } + } + fun valuesAsFlow(sortBy: SortBy, sortReverse: Boolean) = symphony.database.playlists.valuesAsFlow(sortBy, sortReverse) diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/SongRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/SongRepository.kt index 6cb40f27..0710459c 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/SongRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/SongRepository.kt @@ -3,8 +3,9 @@ package io.github.zyrouge.symphony.services.groove.repositories import androidx.core.net.toUri import io.github.zyrouge.symphony.Symphony import io.github.zyrouge.symphony.services.database.store.valuesAsFlow -import io.github.zyrouge.symphony.ui.helpers.Assets -import io.github.zyrouge.symphony.ui.helpers.createHandyImageRequest +import io.github.zyrouge.symphony.services.groove.entities.SongArtworkIndex +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.mapLatest class SongRepository(private val symphony: Symphony) { enum class SortBy { @@ -21,16 +22,14 @@ class SongRepository(private val symphony: Symphony) { TRACK_NUMBER, } - fun createArtworkImageRequest(songId: String) = createHandyImageRequest( - symphony.applicationContext, - image = getArtworkUri(songId), - fallback = Assets.getPlaceholderId(symphony), - ) + @OptIn(ExperimentalCoroutinesApi::class) + fun getArtworkUriAsFlow(id: String) = + symphony.database.songArtworkIndices.findBySongIdAsFlow(id) + .mapLatest { index -> index?.let { getArtworkUriFromIndex(it) } } - fun getArtworkUri(songId: String) = - symphony.database.songArtworkIndices.findBySongId(songId)?.file - ?.let { symphony.database.songArtworks.get(it).toUri() } - ?: Assets.getPlaceholderUri(symphony) + fun getArtworkUriFromIndex(index: SongArtworkIndex) = index.file?.let { + symphony.database.songArtworks.get(it).toUri() + } fun valuesAsFlow(sortBy: SortBy, sortReverse: Boolean) = symphony.database.songs.valuesAsFlow(sortBy, sortReverse) diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/helpers/Assets.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/helpers/Assets.kt index 8c488e2c..dcefe4b6 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/helpers/Assets.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/helpers/Assets.kt @@ -23,9 +23,11 @@ object Assets { .isLight(), ) - fun getPlaceholderUri(symphony: Symphony) = buildUriOfResource( + fun getPlaceholderUri(symphony: Symphony) = getResourceUri(symphony, getPlaceholderId(symphony)) + + fun getResourceUri(symphony: Symphony, resourceId: Int) = buildUriOfResource( symphony.applicationContext.resources, - getPlaceholderId(symphony), + resourceId, ) fun createPlaceholderImageRequest(symphony: Symphony) = From 8d90131065b2d7e9db3051935423549ca8864edb Mon Sep 17 00:00:00 2001 From: Zyrouge Date: Sun, 16 Feb 2025 23:07:59 +0530 Subject: [PATCH 07/15] refactor: more queries --- .../zyrouge/symphony/services/Settings.kt | 24 +- .../database/store/AlbumArtistMappingStore.kt | 32 -- .../services/database/store/AlbumStore.kt | 25 +- .../services/database/store/ArtistStore.kt | 33 +- .../services/database/store/GenreStore.kt | 15 + .../database/store/MediaTreeFolderStore.kt | 32 +- .../database/store/MediaTreeLyricFileStore.kt | 4 +- .../store/PlaylistSongMappingStore.kt | 43 +- .../services/database/store/PlaylistStore.kt | 14 + .../store/SongQueueSongMappingStore.kt | 2 +- .../services/database/store/SongStore.kt | 28 +- .../symphony/services/groove/Groove.kt | 2 - .../services/groove/entities/Album.kt | 2 +- .../services/groove/entities/Artist.kt | 2 +- .../services/groove/entities/Composer.kt | 2 +- .../services/groove/entities/Genre.kt | 2 +- .../groove/entities/MediaTreeFolder.kt | 12 + .../services/groove/entities/Playlist.kt | 17 +- .../symphony/services/groove/entities/Song.kt | 2 +- .../services/groove/entities/SongQueue.kt | 2 +- .../repositories/AlbumArtistRepository.kt | 16 - .../groove/repositories/AlbumRepository.kt | 7 + .../groove/repositories/ArtistRepository.kt | 26 +- .../groove/repositories/GenreRepository.kt | 12 + .../repositories/MediaTreeRepository.kt | 29 ++ .../groove/repositories/PlaylistRepository.kt | 69 ++- .../symphony/services/radio/RadioQueue.kt | 13 + .../symphony/services/radio/RadioShorty.kt | 53 ++- .../ui/components/AddToPlaylistDialog.kt | 5 +- .../symphony/ui/components/AlbumArtistGrid.kt | 6 +- .../symphony/ui/components/AlbumGrid.kt | 5 +- .../symphony/ui/components/AlbumRow.kt | 13 +- .../symphony/ui/components/ArtistGrid.kt | 5 +- .../ui/components/GenericGrooveBanner.kt | 73 +++- .../ui/components/GenericSongListDropdown.kt | 4 +- .../symphony/ui/components/GenreGrid.kt | 11 +- .../ui/components/IntroductoryDialog.kt | 5 +- .../symphony/ui/components/LyricsText.kt | 5 +- .../ui/components/NewPlaylistDialog.kt | 15 +- .../ui/components/NowPlayingBottomBar.kt | 19 +- .../symphony/ui/components/PlaylistGrid.kt | 5 +- .../components/PlaylistManageSongsDialog.kt | 24 +- .../symphony/ui/components/PlaylistTile.kt | 35 +- .../symphony/ui/components/SongCard.kt | 16 +- .../ui/components/SongExplorerList.kt | 5 +- .../symphony/ui/components/SongTreeList.kt | 15 +- .../github/zyrouge/symphony/ui/theme/Theme.kt | 13 +- .../github/zyrouge/symphony/ui/view/Album.kt | 83 ++-- .../zyrouge/symphony/ui/view/AlbumArtist.kt | 156 ------- .../github/zyrouge/symphony/ui/view/Artist.kt | 78 ++-- .../github/zyrouge/symphony/ui/view/Base.kt | 3 - .../github/zyrouge/symphony/ui/view/Genre.kt | 54 ++- .../github/zyrouge/symphony/ui/view/Home.kt | 9 +- .../github/zyrouge/symphony/ui/view/Lyrics.kt | 8 +- .../zyrouge/symphony/ui/view/NowPlaying.kt | 37 +- .../zyrouge/symphony/ui/view/Playlist.kt | 116 +++-- .../github/zyrouge/symphony/ui/view/Queue.kt | 5 +- .../symphony/ui/view/home/AlbumArtists.kt | 14 +- .../zyrouge/symphony/ui/view/home/Albums.kt | 11 +- .../zyrouge/symphony/ui/view/home/Artists.kt | 11 +- .../zyrouge/symphony/ui/view/home/Browser.kt | 26 +- .../zyrouge/symphony/ui/view/home/Folders.kt | 399 +++--------------- .../zyrouge/symphony/ui/view/home/ForYou.kt | 23 +- .../zyrouge/symphony/ui/view/home/Genres.kt | 10 +- .../symphony/ui/view/home/Playlists.kt | 26 +- .../zyrouge/symphony/ui/view/home/Songs.kt | 11 +- .../zyrouge/symphony/ui/view/home/Tree.kt | 32 +- .../ui/view/nowPlaying/BodyContent.kt | 5 +- .../symphony/ui/view/nowPlaying/BodyCover.kt | 7 +- .../symphony/ui/view/nowPlaying/BottomBar.kt | 5 +- .../view/settings/AppearanceSettingsView.kt | 15 +- .../ui/view/settings/GrooveSettingsView.kt | 21 +- .../ui/view/settings/HomePageSettingsView.kt | 7 +- .../view/settings/MiniPlayerSettingsView.kt | 7 +- .../view/settings/NowPlayingSettingsView.kt | 11 +- .../ui/view/settings/PlayerSettingsView.kt | 19 +- .../ui/view/settings/UpdateSettingsView.kt | 5 +- 77 files changed, 949 insertions(+), 1064 deletions(-) delete mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumArtistRepository.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/MediaTreeRepository.kt delete mode 100644 app/src/main/java/io/github/zyrouge/symphony/ui/view/AlbumArtist.kt diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/Settings.kt b/app/src/main/java/io/github/zyrouge/symphony/services/Settings.kt index 96109d1b..05f78120 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/Settings.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/Settings.kt @@ -4,10 +4,10 @@ import android.content.Context import android.net.Uri import androidx.core.content.edit import io.github.zyrouge.symphony.Symphony -import io.github.zyrouge.symphony.services.groove.repositories.AlbumArtistRepository import io.github.zyrouge.symphony.services.groove.repositories.AlbumRepository import io.github.zyrouge.symphony.services.groove.repositories.ArtistRepository import io.github.zyrouge.symphony.services.groove.repositories.GenreRepository +import io.github.zyrouge.symphony.services.groove.repositories.MediaTreeRepository import io.github.zyrouge.symphony.services.groove.repositories.PlaylistRepository import io.github.zyrouge.symphony.services.groove.repositories.SongRepository import io.github.zyrouge.symphony.services.radio.RadioQueue @@ -145,8 +145,8 @@ class Settings(private val symphony: Symphony) { ) val lastUsedAlbumArtistsSortBy = EnumEntry( "last_used_album_artists_sort_by", - enumEntries(), - AlbumArtistRepository.SortBy.ARTIST_NAME, + enumEntries(), + ArtistRepository.SortBy.ARTIST_NAME, ) val lastUsedAlbumArtistsSortReverse = BooleanEntry("last_used_album_artists_sort_reverse", false) @@ -199,6 +199,12 @@ class Settings(private val symphony: Symphony) { PlaylistRepository.SortBy.CUSTOM, ) val lastUsedPlaylistsSortReverse = BooleanEntry("last_used_playlists_sort_reverse", false) + val lastUsedFoldersSortBy = EnumEntry( + "last_used_folders_sort_by", + enumEntries(), + MediaTreeRepository.SortBy.TITLE, + ) + val lastUsedFoldersSortReverse = BooleanEntry("last_used_folders_sort_reverse", false) val lastUsedPlaylistsHorizontalGridColumns = IntEntry( "last_used_playlists_horizontal_grid_columns", ResponsiveGridColumns.DEFAULT_HORIZONTAL_COLUMNS, @@ -220,18 +226,18 @@ class Settings(private val symphony: Symphony) { SongRepository.SortBy.TRACK_NUMBER, ) val lastUsedAlbumSongsSortReverse = BooleanEntry("last_used_album_songs_sort_reverse", false) + val lastUsedArtistSongsSortBy = EnumEntry( + "last_used_artist_songs_sort_by", + enumEntries(), + SongRepository.SortBy.YEAR, + ) + val lastUsedArtistSongsSortReverse = BooleanEntry("last_used_artist_songs_sort_reverse", false) val lastUsedTreePathSortBy = EnumEntry( "last_used_tree_path_sort_by", enumEntries(), StringListUtils.SortBy.NAME, ) val lastUsedTreePathSortReverse = BooleanEntry("last_used_tree_path_sort_reverse", false) - val lastUsedFoldersSortBy = EnumEntry( - "last_used_folders_sort_by", - enumEntries(), - StringListUtils.SortBy.NAME, - ) - val lastUsedFoldersSortReverse = BooleanEntry("last_used_folders_sort_reverse", false) val lastUsedFoldersHorizontalGridColumns = IntEntry( "last_used_folders_horizontal_grid_columns", ResponsiveGridColumns.DEFAULT_HORIZONTAL_COLUMNS, diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumArtistMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumArtistMappingStore.kt index c37e5a6d..d1d340ef 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumArtistMappingStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumArtistMappingStore.kt @@ -3,42 +3,10 @@ package io.github.zyrouge.symphony.services.database.store import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy -import androidx.room.RawQuery -import androidx.sqlite.db.SimpleSQLiteQuery -import androidx.sqlite.db.SupportSQLiteQuery import io.github.zyrouge.symphony.services.groove.entities.AlbumArtistMapping -import io.github.zyrouge.symphony.services.groove.entities.Artist -import io.github.zyrouge.symphony.services.groove.entities.ArtistSongMapping -import io.github.zyrouge.symphony.services.groove.repositories.AlbumArtistRepository -import kotlinx.coroutines.flow.Flow @Dao interface AlbumArtistMappingStore { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun upsert(vararg entities: AlbumArtistMapping) - - @RawQuery(observedEntities = [AlbumArtistMapping::class, Artist::class, ArtistSongMapping::class]) - fun valuesAsFlowRaw(query: SupportSQLiteQuery): Flow> -} - -fun AlbumArtistMappingStore.valuesAsFlow( - sortBy: AlbumArtistRepository.SortBy, - sortReverse: Boolean, -): Flow> { - val orderBy = when (sortBy) { - AlbumArtistRepository.SortBy.CUSTOM -> "${Artist.TABLE}.${Artist.COLUMN_ID}" - AlbumArtistRepository.SortBy.ARTIST_NAME -> "${Artist.TABLE}.${Artist.COLUMN_NAME}" - AlbumArtistRepository.SortBy.TRACKS_COUNT -> Artist.AlongAttributes.EMBEDDED_TRACKS_COUNT - AlbumArtistRepository.SortBy.ALBUMS_COUNT -> Artist.AlongAttributes.EMBEDDED_ALBUMS_COUNT - } - val orderDirection = if (sortReverse) "DESC" else "ASC" - val query = "SELECT ${Artist.TABLE}.*, " + - "COUNT(${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_SONG_ID}) as ${Artist.AlongAttributes.EMBEDDED_TRACKS_COUNT}, " + - "COUNT(${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID}) as ${Artist.AlongAttributes.EMBEDDED_ALBUMS_COUNT} " + - "FROM ${Artist.TABLE} " + - "LEFT JOIN ${ArtistSongMapping.TABLE} ON ${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_ARTIST_ID} = ${Artist.TABLE}.${Artist.COLUMN_ID} " + - "LEFT JOIN ${AlbumArtistMapping.TABLE} ON ${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ARTIST_ID} = ${Artist.TABLE}.${Artist.COLUMN_ID} " + - "WHERE ${AlbumArtistMapping.COLUMN_IS_ALBUM_ARTIST} = 1 " + - "ORDER BY $orderBy $orderDirection" - return valuesAsFlowRaw(SimpleSQLiteQuery(query)) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumStore.kt index 8290b525..a2b25336 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumStore.kt @@ -25,16 +25,30 @@ interface AlbumStore { @Query("SELECT * FROM ${Album.TABLE} WHERE ${Album.COLUMN_NAME} = :name LIMIT 1") fun findByName(name: String): Album? - @Query("SELECT * FROM ${Album.TABLE} WHERE ${Album.COLUMN_ID} = :id LIMIT 1") - fun findByIdAsFlow(id: String): Flow + @RawQuery(observedEntities = [Album::class, AlbumArtistMapping::class, AlbumSongMapping::class]) + fun findByIdAsFlowRaw(query: SupportSQLiteQuery): Flow @RawQuery(observedEntities = [Album::class, AlbumArtistMapping::class, AlbumSongMapping::class]) fun valuesAsFlowRaw(query: SupportSQLiteQuery): Flow> } +fun AlbumStore.findByIdAsFlow(id: String): Flow { + val query = "SELECT ${Album.TABLE}.*, " + + "COUNT(${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_SONG_ID}) as ${Album.AlongAttributes.EMBEDDED_TRACKS_COUNT}, " + + "COUNT(${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ARTIST_ID}) as ${Album.AlongAttributes.EMBEDDED_ARTISTS_COUNT} " + + "FROM ${Album.TABLE} " + + "LEFT JOIN ${AlbumSongMapping.TABLE} ON ${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_ALBUM_ID} = ${Album.TABLE}.${Album.COLUMN_ID} " + + "LEFT JOIN ${AlbumArtistMapping.TABLE} ON ${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID} = ${Album.TABLE}.${Album.COLUMN_ID} " + + "WHERE ${Album.COLUMN_ID} = ? " + + "LIMIT 1" + val args = arrayOf(id) + return findByIdAsFlowRaw(SimpleSQLiteQuery(query, args)) +} + fun AlbumStore.valuesAsFlow( sortBy: AlbumRepository.SortBy, sortReverse: Boolean, + artistId: String? = null, ): Flow> { val aliasFirstArtist = "firstArtist" val embeddedArtistName = "firstArtistName" @@ -52,14 +66,17 @@ fun AlbumStore.valuesAsFlow( "FROM ${AlbumArtistMapping.TABLE} " + "WHERE ${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID} = ${Album.COLUMN_ID} " + "ORDER BY ${AlbumArtistMapping.COLUMN_IS_ALBUM_ARTIST} DESC" + val albumArtistMappingJoin = "" + + (if (artistId != null) "${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ARTIST_ID} = ? " else "") + + "${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID} = ${Album.TABLE}.${Album.COLUMN_ID}" val query = "SELECT ${Album.TABLE}.*, " + "COUNT(${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_SONG_ID}) as ${Album.AlongAttributes.EMBEDDED_TRACKS_COUNT}, " + "COUNT(${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ARTIST_ID}) as ${Album.AlongAttributes.EMBEDDED_ARTISTS_COUNT}, " + "$aliasFirstArtist.${Artist.COLUMN_NAME} as $embeddedArtistName" + "FROM ${Album.TABLE} " + "LEFT JOIN ${AlbumSongMapping.TABLE} ON ${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_ALBUM_ID} = ${Album.TABLE}.${Album.COLUMN_ID} " + - "LEFT JOIN ${AlbumArtistMapping.TABLE} ON ${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID} = ${Album.TABLE}.${Album.COLUMN_ID} " + - "LEFT JOIN ${Artist.TABLE} $aliasFirstArtist ON ${Artist.TABLE}.${Artist.COLUMN_ID} = ($artistQuery)" + + "LEFT JOIN ${AlbumArtistMapping.TABLE} ON $albumArtistMappingJoin " + + "LEFT JOIN ${Artist.TABLE} $aliasFirstArtist ON ${Artist.TABLE}.${Artist.COLUMN_ID} = ($artistQuery) " + "ORDER BY $orderBy $orderDirection" return valuesAsFlowRaw(SimpleSQLiteQuery(query)) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistStore.kt index e9623573..ae67ee7b 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistStore.kt @@ -9,6 +9,7 @@ import androidx.room.Update import androidx.sqlite.db.SimpleSQLiteQuery import androidx.sqlite.db.SupportSQLiteQuery import io.github.zyrouge.symphony.services.groove.entities.AlbumArtistMapping +import io.github.zyrouge.symphony.services.groove.entities.AlbumSongMapping import io.github.zyrouge.symphony.services.groove.entities.Artist import io.github.zyrouge.symphony.services.groove.entities.ArtistSongMapping import io.github.zyrouge.symphony.services.groove.repositories.ArtistRepository @@ -22,6 +23,9 @@ interface ArtistStore { @Update suspend fun update(vararg entities: Artist): Int + @RawQuery(observedEntities = [Artist::class, ArtistSongMapping::class, AlbumArtistMapping::class]) + fun findByIdAsFlowRaw(query: SupportSQLiteQuery): Flow + @RawQuery(observedEntities = [Artist::class, ArtistSongMapping::class, AlbumArtistMapping::class]) fun valuesAsFlowRaw(query: SupportSQLiteQuery): Flow> @@ -31,9 +35,24 @@ interface ArtistStore { @MapColumn(Artist.COLUMN_ID) String> } +fun ArtistStore.findByIdAsFlow(id: String): Flow { + val query = "SELECT ${Artist.TABLE}.*, " + + "COUNT(${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_SONG_ID}) as ${Artist.AlongAttributes.EMBEDDED_TRACKS_COUNT}, " + + "COUNT(${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID}) as ${Artist.AlongAttributes.EMBEDDED_ALBUMS_COUNT} " + + "FROM ${Artist.TABLE} " + + "LEFT JOIN ${AlbumSongMapping.TABLE} ON ${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_ALBUM_ID} = ${Artist.TABLE}.${Artist.COLUMN_ID} " + + "LEFT JOIN ${AlbumArtistMapping.TABLE} ON ${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID} = ${Artist.TABLE}.${Artist.COLUMN_ID} " + + "WHERE ${Artist.COLUMN_ID} = ? " + + "LIMIT 1" + val args = arrayOf(id) + return findByIdAsFlowRaw(SimpleSQLiteQuery(query, args)) +} + fun ArtistStore.valuesAsFlow( sortBy: ArtistRepository.SortBy, sortReverse: Boolean, + albumId: String? = null, + onlyAlbumArtists: Boolean = false, ): Flow> { val orderBy = when (sortBy) { ArtistRepository.SortBy.CUSTOM -> "${Artist.TABLE}.${Artist.COLUMN_ID}" @@ -42,12 +61,20 @@ fun ArtistStore.valuesAsFlow( ArtistRepository.SortBy.ALBUMS_COUNT -> Artist.AlongAttributes.EMBEDDED_ALBUMS_COUNT } val orderDirection = if (sortReverse) "DESC" else "ASC" + val albumArtistMappingJoin = "" + + (if (albumId != null) "${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID} = ? " else "") + + (if (onlyAlbumArtists) "${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_IS_ALBUM_ARTIST} = 1 " else "") + + "${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ARTIST_ID} = ${Artist.TABLE}.${Artist.COLUMN_ID}" val query = "SELECT ${Artist.TABLE}.*, " + "COUNT(${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_SONG_ID}) as ${Artist.AlongAttributes.EMBEDDED_TRACKS_COUNT}, " + "COUNT(${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID}) as ${Artist.AlongAttributes.EMBEDDED_ALBUMS_COUNT} " + "FROM ${Artist.TABLE} " + - "LEFT JOIN ${ArtistSongMapping.TABLE} ON ${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_ARTIST_ID} = ${Artist.TABLE}.${Artist.COLUMN_ID} " + - "LEFT JOIN ${AlbumArtistMapping.TABLE} ON ${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ARTIST_ID} = ${Artist.TABLE}.${Artist.COLUMN_ID}" + + "LEFT JOIN ${ArtistSongMapping.TABLE} ON ${ArtistSongMapping.TABLE}.${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_ARTIST_ID} = ${Artist.TABLE}.${Artist.COLUMN_ID} " + + "LEFT JOIN ${AlbumArtistMapping.TABLE} ON $albumArtistMappingJoin " + "ORDER BY $orderBy $orderDirection" - return valuesAsFlowRaw(SimpleSQLiteQuery(query)) + val args = mutableListOf() + if (albumId != null) { + args.add(albumId) + } + return valuesAsFlowRaw(SimpleSQLiteQuery(query, args.toTypedArray())) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreStore.kt index 3ac131ec..261ee0f3 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreStore.kt @@ -17,6 +17,9 @@ interface GenreStore { @Insert suspend fun insert(vararg entities: Genre): List + @RawQuery(observedEntities = [Genre::class, GenreSongMapping::class]) + fun findByIdAsFlowRaw(query: SupportSQLiteQuery): Flow + @RawQuery(observedEntities = [Genre::class, GenreSongMapping::class]) fun valuesAsFlowRaw(query: SupportSQLiteQuery): Flow> @@ -26,6 +29,18 @@ interface GenreStore { @MapColumn(Genre.COLUMN_ID) String> } +fun GenreStore.findByIdAsFlow(id: String): Flow { + val query = "SELECT ${Genre.TABLE}.*, " + + "COUNT(${GenreSongMapping.TABLE}.${GenreSongMapping.COLUMN_SONG_ID}) as ${Genre.AlongAttributes.EMBEDDED_TRACKS_COUNT} " + + "FROM ${Genre.TABLE} " + + "LEFT JOIN ${GenreSongMapping.TABLE} ON ${GenreSongMapping.TABLE}.${GenreSongMapping.COLUMN_GENRE_ID} = ${Genre.TABLE}.${Genre.COLUMN_ID} " + + "LEFT JOIN ${GenreSongMapping.TABLE} ON ${GenreSongMapping.TABLE}.${GenreSongMapping.COLUMN_GENRE_ID} = ${Genre.TABLE}.${Genre.COLUMN_ID} " + + "WHERE ${Genre.COLUMN_ID} = ? " + + "LIMIT 1" + val args = arrayOf(id) + return findByIdAsFlowRaw(SimpleSQLiteQuery(query, args)) +} + fun GenreStore.valuesAsFlow( sortBy: GenreRepository.SortBy, sortReverse: Boolean, diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeFolderStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeFolderStore.kt index de4949e3..75941376 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeFolderStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeFolderStore.kt @@ -4,15 +4,21 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.MapColumn import androidx.room.Query +import androidx.room.RawQuery import androidx.room.Update +import androidx.sqlite.db.SimpleSQLiteQuery +import androidx.sqlite.db.SupportSQLiteQuery import io.github.zyrouge.symphony.services.groove.entities.MediaTreeFolder +import io.github.zyrouge.symphony.services.groove.entities.MediaTreeSongFile +import io.github.zyrouge.symphony.services.groove.repositories.MediaTreeRepository +import kotlinx.coroutines.flow.Flow @Dao interface MediaTreeFolderStore { - @Insert() + @Insert suspend fun insert(vararg entities: MediaTreeFolder): List - @Update() + @Update suspend fun update(vararg entities: MediaTreeFolder): Int @Query("SELECT id FROM ${MediaTreeFolder.TABLE} WHERE ${MediaTreeFolder.COLUMN_PARENT_ID} = :parentId") @@ -26,4 +32,26 @@ interface MediaTreeFolderStore { @Query("SELECT * FROM ${MediaTreeFolder.TABLE} WHERE ${MediaTreeFolder.COLUMN_PARENT_ID} = :parentId") fun entriesNameMapped(parentId: String): Map<@MapColumn(MediaTreeFolder.COLUMN_NAME) String, MediaTreeFolder> + + @RawQuery(observedEntities = [MediaTreeFolder::class, MediaTreeSongFile::class]) + fun valuesAsFlowRaw(query: SupportSQLiteQuery): Flow> +} + +fun MediaTreeFolderStore.valuesAsFlow( + sortBy: MediaTreeRepository.SortBy, + sortReverse: Boolean, +): Flow> { + val orderBy = when (sortBy) { + MediaTreeRepository.SortBy.CUSTOM -> "${MediaTreeFolder.TABLE}.${MediaTreeFolder.COLUMN_ID}" + MediaTreeRepository.SortBy.TITLE -> "${MediaTreeFolder.TABLE}.${MediaTreeFolder.COLUMN_NAME}" + MediaTreeRepository.SortBy.TRACKS_COUNT -> MediaTreeFolder.AlongAttributes.EMBEDDED_TRACKS_COUNT + } + val orderDirection = if (sortReverse) "DESC" else "ASC" + val query = "SELECT ${MediaTreeFolder.TABLE}.*, " + + "COUNT(${MediaTreeSongFile.TABLE}.${MediaTreeSongFile.COLUMN_ID}) as ${MediaTreeFolder.AlongAttributes.EMBEDDED_TRACKS_COUNT} " + + "FROM ${MediaTreeFolder.TABLE} " + + "LEFT JOIN ${MediaTreeSongFile.TABLE} ON ${MediaTreeSongFile.TABLE}.${MediaTreeSongFile.COLUMN_PARENT_ID} = ${MediaTreeFolder.TABLE}.${MediaTreeFolder.COLUMN_ID} " + + "WHERE ${MediaTreeFolder.AlongAttributes.EMBEDDED_TRACKS_COUNT} > 0 " + + "ORDER BY $orderBy $orderDirection" + return valuesAsFlowRaw(SimpleSQLiteQuery(query)) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeLyricFileStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeLyricFileStore.kt index 78f0886a..7fda2db4 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeLyricFileStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeLyricFileStore.kt @@ -9,10 +9,10 @@ import io.github.zyrouge.symphony.services.groove.entities.MediaTreeLyricFile @Dao interface MediaTreeLyricFileStore { - @Insert() + @Insert suspend fun insert(vararg entities: MediaTreeLyricFile): List - @Update() + @Update suspend fun update(vararg entities: MediaTreeLyricFile): Int @Query("SELECT id FROM ${MediaTreeLyricFile.TABLE} WHERE ${MediaTreeLyricFile.COLUMN_PARENT_ID} = :parentId") diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistSongMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistSongMappingStore.kt index cb62f999..9e6aa543 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistSongMappingStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistSongMappingStore.kt @@ -5,6 +5,7 @@ import androidx.room.Insert import androidx.room.Query import androidx.room.RawQuery import androidx.sqlite.db.SimpleSQLiteQuery +import io.github.zyrouge.symphony.services.groove.entities.Playlist import io.github.zyrouge.symphony.services.groove.entities.PlaylistSongMapping import io.github.zyrouge.symphony.services.groove.entities.Song import io.github.zyrouge.symphony.services.groove.entities.SongArtworkIndex @@ -23,6 +24,9 @@ interface PlaylistSongMappingStore { @RawQuery(observedEntities = [SongArtworkIndex::class, PlaylistSongMapping::class]) fun findTop4SongArtworksAsFlowRaw(query: SimpleSQLiteQuery): Flow> + + @RawQuery + fun findSongIdsByPlaylistInternalIdAsFlowRaw(query: SimpleSQLiteQuery): Flow> } @OptIn(ExperimentalCoroutinesApi::class) @@ -32,27 +36,29 @@ fun PlaylistSongMappingStore.valuesMappedAsFlow( sortBy: SongRepository.SortBy, sortReverse: Boolean, ): Flow> { - val query = songStore.valuesAsFlowQuery( + val query = songStore.valuesQuery( sortBy, sortReverse, additionalClauseBeforeJoins = "JOIN ${PlaylistSongMapping.TABLE} ON ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_PLAYLIST_ID} = ? AND ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_SONG_ID} = ${Song.COLUMN_ID} ", additionalArgsBeforeJoins = arrayOf(id), ) - val entries = songStore.entriesAsPlaylistSongMappedFlowRaw(query) - return entries.mapLatest { - val list = mutableListOf() - var head = it.firstNotNullOfOrNull { - when { - it.value.mapping.isHead -> it.value - else -> null - } - } - while (head != null) { - list.add(head.song) - head = it[head.mapping.nextId] + val entries = songStore.entriesAsPlaylistSongMappedAsFlowRaw(query) + return entries.mapLatest { transformEntriesAsValues(it) } +} + +fun PlaylistSongMappingStore.transformEntriesAsValues(entries: Map): List { + val list = mutableListOf() + var head = entries.firstNotNullOfOrNull { + when { + it.value.mapping.isHead -> it.value + else -> null } - list.toList() } + while (head != null) { + list.add(head.song) + head = entries[head.mapping.nextId] + } + return list.toList() } fun PlaylistSongMappingStore.findTop4SongArtworksAsFlow(playlistId: String): Flow> { @@ -65,3 +71,12 @@ fun PlaylistSongMappingStore.findTop4SongArtworksAsFlow(playlistId: String): Flo val args = arrayOf(playlistId) return findTop4SongArtworksAsFlowRaw(SimpleSQLiteQuery(query, args)) } + +@OptIn(ExperimentalCoroutinesApi::class) +fun PlaylistSongMappingStore.findSongIdsByPlaylistInternalIdAsFlow(playlistInternalId: Int): Flow> { + val query = "SELECT ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_SONG_ID} " + + "FROM ${PlaylistSongMapping.TABLE} " + + "LEFT JOIN ${Playlist.TABLE} ON ${Playlist.TABLE}.${Playlist.COLUMN_INTERNAL_ID} = ? AND ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_PLAYLIST_ID} = ${Playlist.TABLE}.${Playlist.COLUMN_ID} " + val args = arrayOf(playlistInternalId) + return findSongIdsByPlaylistInternalIdAsFlowRaw(SimpleSQLiteQuery(query, args)) +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistStore.kt index d02cc1bc..52e17561 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistStore.kt @@ -23,6 +23,9 @@ interface PlaylistStore { @Query("DELETE FROM ${Playlist.TABLE} WHERE ${Playlist.COLUMN_ID} = :id") suspend fun delete(id: String): Int + @RawQuery(observedEntities = [Playlist::class, PlaylistSongMapping::class]) + fun findByIdAsFlowRaw(query: SupportSQLiteQuery): Flow + @Query("SELECT * FROM ${Playlist.TABLE} WHERE ${Playlist.COLUMN_URI} != NULL") fun valuesLocalOnly(): List @@ -30,6 +33,17 @@ interface PlaylistStore { fun valuesAsFlowRaw(query: SupportSQLiteQuery): Flow> } +fun PlaylistStore.findByIdAsFlow(id: String): Flow { + val query = "SELECT ${Playlist.TABLE}.*, " + + "COUNT(${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_SONG_ID}) as ${Playlist.AlongAttributes.EMBEDDED_TRACKS_COUNT} " + + "FROM ${Playlist.TABLE} " + + "LEFT JOIN ${PlaylistSongMapping.TABLE} ON ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_PLAYLIST_ID} = ${Playlist.TABLE}.${Playlist.COLUMN_ID} " + + "WHERE ${Playlist.TABLE}.${Playlist.COLUMN_ID} = ? " + + "LIMIT 1" + val args = arrayOf(id) + return findByIdAsFlowRaw(SimpleSQLiteQuery(query, args)) +} + fun PlaylistStore.valuesAsFlow( sortBy: PlaylistRepository.SortBy, sortReverse: Boolean, diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueSongMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueSongMappingStore.kt index 61396573..d3570912 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueSongMappingStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueSongMappingStore.kt @@ -47,7 +47,7 @@ fun SongQueueSongMappingStore.transformEntriesAsValuesFlow(entries: Flow> + fun entriesAsPlaylistSongMappedAsFlowRaw(query: SupportSQLiteQuery): Flow> + + @RawQuery + fun valuesRaw(query: SupportSQLiteQuery): List @RawQuery( observedEntities = [ @@ -78,7 +81,7 @@ interface SongStore { fun valuesAsFlowRaw(query: SupportSQLiteQuery): Flow> } -internal fun SongStore.valuesAsFlowQuery( +internal fun SongStore.valuesQuery( sortBy: SongRepository.SortBy, sortReverse: Boolean, additionalClauseAfterSongSelect: String = "", @@ -138,6 +141,25 @@ internal fun SongStore.valuesAsFlowQuery( return SimpleSQLiteQuery(query, args) } +fun SongStore.values( + sortBy: SongRepository.SortBy, + sortReverse: Boolean, + additionalClauseAfterSongSelect: String = "", + additionalClauseBeforeJoins: String = "", + additionalArgsBeforeJoins: Array = emptyArray(), + overrideOrderBy: String? = null, +): List { + val query = valuesQuery( + sortBy = sortBy, + sortReverse = sortReverse, + additionalClauseAfterSongSelect = additionalClauseAfterSongSelect, + additionalClauseBeforeJoins = additionalClauseBeforeJoins, + additionalArgsBeforeJoins = additionalArgsBeforeJoins, + overrideOrderBy = overrideOrderBy, + ) + return valuesRaw(query) +} + fun SongStore.valuesAsFlow( sortBy: SongRepository.SortBy, sortReverse: Boolean, @@ -146,7 +168,7 @@ fun SongStore.valuesAsFlow( additionalArgsBeforeJoins: Array = emptyArray(), overrideOrderBy: String? = null, ): Flow> { - val query = valuesAsFlowQuery( + val query = valuesQuery( sortBy = sortBy, sortReverse = sortReverse, additionalClauseAfterSongSelect = additionalClauseAfterSongSelect, diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/Groove.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/Groove.kt index 5bf5f313..5861401f 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/Groove.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/Groove.kt @@ -1,7 +1,6 @@ package io.github.zyrouge.symphony.services.groove import io.github.zyrouge.symphony.Symphony -import io.github.zyrouge.symphony.services.groove.repositories.AlbumArtistRepository import io.github.zyrouge.symphony.services.groove.repositories.AlbumRepository import io.github.zyrouge.symphony.services.groove.repositories.ArtistRepository import io.github.zyrouge.symphony.services.groove.repositories.GenreRepository @@ -29,7 +28,6 @@ class Groove(private val symphony: Symphony) : Symphony.Hooks { val song = SongRepository(symphony) val album = AlbumRepository(symphony) val artist = ArtistRepository(symphony) - val albumArtist = AlbumArtistRepository(symphony) val genre = GenreRepository(symphony) val playlist = PlaylistRepository(symphony) diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Album.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Album.kt index 181adaa5..f1255f28 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Album.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Album.kt @@ -25,7 +25,7 @@ data class Album( ) { data class AlongAttributes( @Embedded - val album: Album, + val entity: Album, @Embedded val tracksCount: Int, @Embedded diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Artist.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Artist.kt index f50f2f2d..2d568a0f 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Artist.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Artist.kt @@ -22,7 +22,7 @@ data class Artist( ) { data class AlongAttributes( @Embedded - val artist: Artist, + val entity: Artist, @Embedded val tracksCount: Int, @Embedded diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Composer.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Composer.kt index 9ebbb56f..81ffa539 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Composer.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Composer.kt @@ -21,7 +21,7 @@ data class Composer( ) { data class AlongAttributes( @Embedded - val composer: Composer, + val entity: Composer, @Embedded val tracksCount: Int, @Embedded diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Genre.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Genre.kt index 076bfedd..ca5072d6 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Genre.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Genre.kt @@ -21,7 +21,7 @@ data class Genre( ) { data class AlongAttributes( @Embedded - val genre: Genre, + val entity: Genre, @Embedded val tracksCount: Int, ) { diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/MediaTreeFolder.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/MediaTreeFolder.kt index dd701745..d002796b 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/MediaTreeFolder.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/MediaTreeFolder.kt @@ -3,6 +3,7 @@ package io.github.zyrouge.symphony.services.groove.entities import android.net.Uri import androidx.compose.runtime.Immutable import androidx.room.ColumnInfo +import androidx.room.Embedded import androidx.room.Entity import androidx.room.ForeignKey import androidx.room.Index @@ -40,6 +41,17 @@ data class MediaTreeFolder( @ColumnInfo(COLUMN_URI) val uri: Uri?, ) { + data class AlongAttributes( + @Embedded + val folder: MediaTreeFolder, + @Embedded + val tracksCount: Int, + ) { + companion object { + const val EMBEDDED_TRACKS_COUNT = "tracksCount" + } + } + companion object { const val TABLE = "media_tree_folders" const val COLUMN_ID = "id" diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Playlist.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Playlist.kt index 8221fd1f..e5070c37 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Playlist.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Playlist.kt @@ -15,12 +15,17 @@ import kotlin.io.path.nameWithoutExtension @Immutable @Entity( Playlist.TABLE, - indices = [Index(Playlist.COLUMN_TITLE)], + indices = [ + Index(Playlist.COLUMN_INTERNAL_ID, unique = true), + Index(Playlist.COLUMN_TITLE), + ], ) data class Playlist( @PrimaryKey @ColumnInfo(COLUMN_ID) val id: String, + @ColumnInfo(COLUMN_INTERNAL_ID) + val internalId: Int? = null, @ColumnInfo(COLUMN_TITLE) val title: String, @ColumnInfo(COLUMN_URI) @@ -28,9 +33,13 @@ data class Playlist( @ColumnInfo(COLUMN_PATH) val path: String?, ) { + val isLocal get() = uri != null + val isModifiable get() = !isLocal + val isInternal get() = internalId != null + data class AlongAttributes( @Embedded - val playlist: Playlist, + val entity: Playlist, @Embedded val tracksCount: Int, ) { @@ -44,13 +53,11 @@ data class Playlist( companion object { const val TABLE = "playlists" const val COLUMN_ID = "id" + const val COLUMN_INTERNAL_ID = "internal_id" const val COLUMN_TITLE = "title" const val COLUMN_URI = "title" const val COLUMN_PATH = "path" - private const val PRIMARY_STORAGE = "primary:" - const val MIMETYPE_M3U = "" - fun parse(symphony: Symphony, id: String, uri: Uri): Parsed { val file = DocumentFileX.fromSingleUri(symphony.applicationContext, uri)!! val content = symphony.applicationContext.contentResolver.openInputStream(uri) diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Song.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Song.kt index ab9b9b2f..23b7facb 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Song.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Song.kt @@ -69,7 +69,7 @@ data class Song( ) { data class AlongSongQueueMapping( @Embedded - val song: Song, + val entity: Song, @Embedded val mapping: SongQueueSongMapping, ) diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueue.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueue.kt index 96ae3ce7..728985ef 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueue.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueue.kt @@ -44,7 +44,7 @@ data class SongQueue( data class AlongAttributes( @Embedded - val queue: SongQueue, + val entity: SongQueue, @Embedded val tracksCount: Int, ) { diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumArtistRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumArtistRepository.kt deleted file mode 100644 index dc4b7ca2..00000000 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumArtistRepository.kt +++ /dev/null @@ -1,16 +0,0 @@ -package io.github.zyrouge.symphony.services.groove.repositories - -import io.github.zyrouge.symphony.Symphony -import io.github.zyrouge.symphony.services.database.store.valuesAsFlow - -class AlbumArtistRepository(private val symphony: Symphony) { - enum class SortBy { - CUSTOM, - ARTIST_NAME, - TRACKS_COUNT, - ALBUMS_COUNT, - } - - fun valuesAsFlow(sortBy: SortBy, sortReverse: Boolean) = - symphony.database.albumArtistMapping.valuesAsFlow(sortBy, sortReverse) -} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumRepository.kt index 6e440713..ed225956 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumRepository.kt @@ -1,6 +1,7 @@ package io.github.zyrouge.symphony.services.groove.repositories import io.github.zyrouge.symphony.Symphony +import io.github.zyrouge.symphony.services.database.store.findByIdAsFlow import io.github.zyrouge.symphony.services.database.store.findTop4SongArtworksAsFlow import io.github.zyrouge.symphony.services.database.store.valuesAsFlow import io.github.zyrouge.symphony.services.database.store.valuesMappedAsFlow @@ -19,6 +20,12 @@ class AlbumRepository(private val symphony: Symphony) { fun findByIdAsFlow(id: String) = symphony.database.albums.findByIdAsFlow(id) + fun findArtistsOfIdAsFlow(id: String) = symphony.database.artists.valuesAsFlow( + ArtistRepository.SortBy.ARTIST_NAME, + false, + albumId = id, + ) + fun findSongsByIdAsFlow(id: String, sortBy: SongRepository.SortBy, sortReverse: Boolean) = symphony.database.albumSongMapping.valuesMappedAsFlow( symphony.database.songs, diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/ArtistRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/ArtistRepository.kt index 527a2f96..f62de8d0 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/ArtistRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/ArtistRepository.kt @@ -1,8 +1,10 @@ package io.github.zyrouge.symphony.services.groove.repositories import io.github.zyrouge.symphony.Symphony +import io.github.zyrouge.symphony.services.database.store.findByIdAsFlow import io.github.zyrouge.symphony.services.database.store.findTop4SongArtworksAsFlow import io.github.zyrouge.symphony.services.database.store.valuesAsFlow +import io.github.zyrouge.symphony.services.database.store.valuesMappedAsFlow import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.mapLatest @@ -14,6 +16,22 @@ class ArtistRepository(private val symphony: Symphony) { ALBUMS_COUNT, } + fun findByIdAsFlow(id: String) = symphony.database.artists.findByIdAsFlow(id) + + fun findAlbumsOfIdAsFlow(id: String) = symphony.database.albums.valuesAsFlow( + AlbumRepository.SortBy.ARTIST_NAME, + false, + artistId = id, + ) + + fun findSongsByIdAsFlow(id: String, sortBy: SongRepository.SortBy, sortReverse: Boolean) = + symphony.database.artistSongMapping.valuesMappedAsFlow( + symphony.database.songs, + id, + sortBy, + sortReverse + ) + @OptIn(ExperimentalCoroutinesApi::class) fun getTop4ArtworkUriAsFlow(id: String) = symphony.database.artistSongMapping.findTop4SongArtworksAsFlow(id) @@ -21,6 +39,10 @@ class ArtistRepository(private val symphony: Symphony) { indices.map { symphony.groove.song.getArtworkUriFromIndex(it) } } - fun valuesAsFlow(sortBy: SortBy, sortReverse: Boolean) = - symphony.database.artists.valuesAsFlow(sortBy, sortReverse) + fun valuesAsFlow(sortBy: SortBy, sortReverse: Boolean, onlyAlbumArtists: Boolean = false) = + symphony.database.artists.valuesAsFlow( + sortBy, + sortReverse, + onlyAlbumArtists = onlyAlbumArtists + ) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/GenreRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/GenreRepository.kt index ed4dc71f..5dbb3778 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/GenreRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/GenreRepository.kt @@ -1,7 +1,9 @@ package io.github.zyrouge.symphony.services.groove.repositories import io.github.zyrouge.symphony.Symphony +import io.github.zyrouge.symphony.services.database.store.findByIdAsFlow import io.github.zyrouge.symphony.services.database.store.valuesAsFlow +import io.github.zyrouge.symphony.services.database.store.valuesMappedAsFlow class GenreRepository(private val symphony: Symphony) { enum class SortBy { @@ -10,6 +12,16 @@ class GenreRepository(private val symphony: Symphony) { TRACKS_COUNT, } + fun findByIdAsFlow(id: String) = symphony.database.genres.findByIdAsFlow(id) + + fun findSongsByIdAsFlow(id: String, sortBy: SongRepository.SortBy, sortReverse: Boolean) = + symphony.database.genreSongMapping.valuesMappedAsFlow( + symphony.database.songs, + id, + sortBy, + sortReverse + ) + fun valuesAsFlow(sortBy: SortBy, sortReverse: Boolean) = symphony.database.genres.valuesAsFlow(sortBy, sortReverse) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/MediaTreeRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/MediaTreeRepository.kt new file mode 100644 index 00000000..470bf7c0 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/MediaTreeRepository.kt @@ -0,0 +1,29 @@ +package io.github.zyrouge.symphony.services.groove.repositories + +import io.github.zyrouge.symphony.Symphony +import io.github.zyrouge.symphony.services.database.store.findTop4SongArtworksAsFlow +import io.github.zyrouge.symphony.services.database.store.valuesAsFlow +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.mapLatest + +class MediaTreeRepository(private val symphony: Symphony) { + enum class SortBy { + CUSTOM, + TITLE, + TRACKS_COUNT, + } + + @OptIn(ExperimentalCoroutinesApi::class) + fun getTop4ArtworkUriAsFlow(id: String) = + symphony.database.playlistSongMapping.findTop4SongArtworksAsFlow(id) + .mapLatest { indices -> + indices.map { symphony.groove.song.getArtworkUriFromIndex(it) } + } + + fun valuesAsFlow(sortBy: SortBy, sortReverse: Boolean) = + symphony.database.playlists.valuesAsFlow(sortBy, sortReverse) + + companion object { + private const val FAVORITE_PLAYLIST = "favorites" + } +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/PlaylistRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/PlaylistRepository.kt index 76bc8ada..1cbd0726 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/PlaylistRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/PlaylistRepository.kt @@ -1,10 +1,17 @@ package io.github.zyrouge.symphony.services.groove.repositories import io.github.zyrouge.symphony.Symphony +import io.github.zyrouge.symphony.services.database.store.findByIdAsFlow +import io.github.zyrouge.symphony.services.database.store.findSongIdsByPlaylistInternalIdAsFlow import io.github.zyrouge.symphony.services.database.store.findTop4SongArtworksAsFlow import io.github.zyrouge.symphony.services.database.store.valuesAsFlow +import io.github.zyrouge.symphony.services.database.store.valuesMappedAsFlow +import io.github.zyrouge.symphony.services.groove.entities.Playlist +import io.github.zyrouge.symphony.services.groove.entities.PlaylistSongMapping import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.launch class PlaylistRepository(private val symphony: Symphony) { enum class SortBy { @@ -13,6 +20,66 @@ class PlaylistRepository(private val symphony: Symphony) { TRACKS_COUNT, } + private val favoriteSongIdsFlow = symphony.database.playlistSongMapping + .findSongIdsByPlaylistInternalIdAsFlow(PLAYLIST_INTERNAL_ID_FAVORITES) + private var favoriteSongIds = emptyList() + + init { + symphony.groove.coroutineScope.launch { + favoriteSongIdsFlow.collectLatest { + favoriteSongIds = it + } + } + } + + data class AddOptions( + val playlist: Playlist, + val songIds: List = emptyList(), + val songPaths: List = emptyList(), + ) + + fun add(options: AddOptions) { + val mappings = mutableListOf() + var nextId: String? = null + for (i in (options.songPaths.size - 1) downTo 0) { + val mapping = PlaylistSongMapping( + id = symphony.database.playlistSongMappingIdGenerator.next(), + playlistId = options.playlist.id, + songId = null, + songPath = options.songPaths[i], + isHead = i == 0, + nextId = nextId, + ) + mappings.add(mapping) + nextId = mapping.id + } + symphony.groove.coroutineScope.launch { + symphony.database.playlists.insert(options.playlist) + symphony.database.playlistSongMapping.insert(*mappings.toTypedArray()) + } + } + + fun removeSongs(playlistId: String, songIds: List) { + // TODO: implement this + } + + fun isFavoriteSong(songId: String) = favoriteSongIds.contains(songId) + + @OptIn(ExperimentalCoroutinesApi::class) + fun isFavoriteSongAsFlow(songId: String) = favoriteSongIdsFlow.mapLatest { + it.contains(songId) + } + + fun findByIdAsFlow(id: String) = symphony.database.playlists.findByIdAsFlow(id) + + fun findSongsByIdAsFlow(id: String, sortBy: SongRepository.SortBy, sortReverse: Boolean) = + symphony.database.playlistSongMapping.valuesMappedAsFlow( + symphony.database.songs, + id, + sortBy, + sortReverse + ) + @OptIn(ExperimentalCoroutinesApi::class) fun getTop4ArtworkUriAsFlow(id: String) = symphony.database.playlistSongMapping.findTop4SongArtworksAsFlow(id) @@ -24,6 +91,6 @@ class PlaylistRepository(private val symphony: Symphony) { symphony.database.playlists.valuesAsFlow(sortBy, sortReverse) companion object { - private const val FAVORITE_PLAYLIST = "favorites" + const val PLAYLIST_INTERNAL_ID_FAVORITES = 1 } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioQueue.kt b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioQueue.kt index d04d42df..c99fc10a 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioQueue.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioQueue.kt @@ -1,6 +1,7 @@ package io.github.zyrouge.symphony.services.radio import io.github.zyrouge.symphony.Symphony +import io.github.zyrouge.symphony.services.groove.entities.Song import io.github.zyrouge.symphony.utils.concurrentListOf class RadioQueue(private val symphony: Symphony) { @@ -72,6 +73,18 @@ class RadioQueue(private val symphony: Symphony) { options: Radio.PlayOptions = Radio.PlayOptions(), ) = add(listOf(songId), index, options) + fun add( + songs: List, + index: Int? = null, + options: Radio.PlayOptions = Radio.PlayOptions(), + ) = add(songs.map { it.id }, index, options) + + fun add( + song: Song, + index: Int? = null, + options: Radio.PlayOptions = Radio.PlayOptions(), + ) = add(listOf(song.id), index, options) + private fun afterAdd(options: Radio.PlayOptions) { if (!symphony.radio.hasPlayer) { symphony.radio.play(options) diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioShorty.kt b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioShorty.kt index d73c7280..9cae963d 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioShorty.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioShorty.kt @@ -1,6 +1,7 @@ package io.github.zyrouge.symphony.services.radio import io.github.zyrouge.symphony.Symphony +import io.github.zyrouge.symphony.services.groove.entities.Song import kotlin.random.Random class RadioShorty(private val symphony: Symphony) { @@ -24,33 +25,29 @@ class RadioShorty(private val symphony: Symphony) { } } - fun previous(): Boolean { - return when { - !symphony.radio.hasPlayer -> false - symphony.radio.currentPlaybackPosition!!.played <= 3000 && symphony.radio.canJumpToPrevious() -> { - symphony.radio.jumpToPrevious() - true - } + fun previous() = when { + !symphony.radio.hasPlayer -> false + symphony.radio.currentPlaybackPosition!!.played <= 3000 && symphony.radio.canJumpToPrevious() -> { + symphony.radio.jumpToPrevious() + true + } - else -> { - symphony.radio.seek(0) - false - } + else -> { + symphony.radio.seek(0) + false } } - fun skip(): Boolean { - return when { - !symphony.radio.hasPlayer -> false - symphony.radio.canJumpToNext() -> { - symphony.radio.jumpToNext() - true - } + fun skip() = when { + !symphony.radio.hasPlayer -> false + symphony.radio.canJumpToNext() -> { + symphony.radio.jumpToNext() + true + } - else -> { - symphony.radio.play(Radio.PlayOptions(index = 0, autostart = false)) - false - } + else -> { + symphony.radio.play(Radio.PlayOptions(index = 0, autostart = false)) + false } } @@ -77,4 +74,16 @@ class RadioShorty(private val symphony: Symphony) { options: Radio.PlayOptions = Radio.PlayOptions(), shuffle: Boolean = false, ) = playQueue(listOf(songId), options = options, shuffle = shuffle) + + fun playQueue( + songs: List, + options: Radio.PlayOptions = Radio.PlayOptions(), + shuffle: Boolean = false, + ) = playQueue(songs.map { it.id }, options = options, shuffle = shuffle) + + fun playQueue( + song: Song, + options: Radio.PlayOptions = Radio.PlayOptions(), + shuffle: Boolean = false, + ) = playQueue(listOf(song.id), options = options, shuffle = shuffle) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/AddToPlaylistDialog.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/AddToPlaylistDialog.kt index afc83a04..a24e8409 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/AddToPlaylistDialog.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/AddToPlaylistDialog.kt @@ -5,14 +5,12 @@ import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -31,7 +29,8 @@ fun AddToPlaylistDialog( onDismissRequest: () -> Unit, ) { var showNewPlaylistDialog by remember { mutableStateOf(false) } - val allPlaylistsIds by context.symphony.groove.playlist.all.collectAsState() + val allPlaylistsIds by context.symphony.groove.playlist.valuesAsFlow() + .collectAsStateWithLifecycle() val playlists by remember(allPlaylistsIds) { derivedStateOf { allPlaylistsIds diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumArtistGrid.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumArtistGrid.kt index 4597620a..17442193 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumArtistGrid.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumArtistGrid.kt @@ -7,7 +7,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -15,7 +14,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import io.github.zyrouge.symphony.services.groove.Groove import io.github.zyrouge.symphony.services.groove.entities.Artist -import io.github.zyrouge.symphony.services.groove.repositories.AlbumArtistRepository import io.github.zyrouge.symphony.ui.helpers.ViewContext @OptIn(ExperimentalMaterial3Api::class) @@ -26,8 +24,8 @@ fun AlbumArtistGrid( sortBy: AlbumArtistRepository.SortBy, sortReverse: Boolean, ) { - val horizontalGridColumns by context.symphony.settings.lastUsedAlbumArtistsHorizontalGridColumns.flow.collectAsState() - val verticalGridColumns by context.symphony.settings.lastUsedAlbumArtistsVerticalGridColumns.flow.collectAsState() + val horizontalGridColumns by context.symphony.settings.lastUsedAlbumArtistsHorizontalGridColumns.flow.collectAsStateWithLifecycle() + val verticalGridColumns by context.symphony.settings.lastUsedAlbumArtistsVerticalGridColumns.flow.collectAsStateWithLifecycle() val gridColumns by remember(horizontalGridColumns, verticalGridColumns) { derivedStateOf { ResponsiveGridColumns(horizontalGridColumns, verticalGridColumns) diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumGrid.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumGrid.kt index 03a85388..9cb8d769 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumGrid.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumGrid.kt @@ -7,7 +7,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -26,8 +25,8 @@ fun AlbumGrid( sortBy: AlbumRepository.SortBy, sortReverse: Boolean, ) { - val horizontalGridColumns by context.symphony.settings.lastUsedAlbumsHorizontalGridColumns.flow.collectAsState() - val verticalGridColumns by context.symphony.settings.lastUsedAlbumsVerticalGridColumns.flow.collectAsState() + val horizontalGridColumns by context.symphony.settings.lastUsedAlbumsHorizontalGridColumns.flow.collectAsStateWithLifecycle() + val verticalGridColumns by context.symphony.settings.lastUsedAlbumsVerticalGridColumns.flow.collectAsStateWithLifecycle() val gridColumns by remember(horizontalGridColumns, verticalGridColumns) { derivedStateOf { ResponsiveGridColumns(horizontalGridColumns, verticalGridColumns) diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumRow.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumRow.kt index c9084722..55e98202 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumRow.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumRow.kt @@ -10,10 +10,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.min import io.github.zyrouge.symphony.services.groove.Groove +import io.github.zyrouge.symphony.services.groove.entities.Album import io.github.zyrouge.symphony.ui.helpers.ViewContext @Composable -fun AlbumRow(context: ViewContext, albumIds: List) { +fun AlbumRow(context: ViewContext, albums: List) { BoxWithConstraints { val maxSize = min( this@BoxWithConstraints.maxHeight, @@ -23,14 +24,12 @@ fun AlbumRow(context: ViewContext, albumIds: List) { LazyRow { itemsIndexed( - albumIds, + albums, key = { i, x -> "$i-$x" }, contentType = { _, _ -> Groove.Kind.ALBUM } - ) { _, albumId -> - context.symphony.groove.album.get(albumId)?.let { album -> - Box(modifier = Modifier.width(width)) { - AlbumTile(context, album) - } + ) { _, album -> + Box(modifier = Modifier.width(width)) { + AlbumTile(context, album) } } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/ArtistGrid.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/ArtistGrid.kt index 40dc1f26..85425787 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/ArtistGrid.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/ArtistGrid.kt @@ -7,7 +7,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -26,8 +25,8 @@ fun ArtistGrid( sortBy: ArtistRepository.SortBy, sortReverse: Boolean, ) { - val horizontalGridColumns by context.symphony.settings.lastUsedArtistsHorizontalGridColumns.flow.collectAsState() - val verticalGridColumns by context.symphony.settings.lastUsedArtistsVerticalGridColumns.flow.collectAsState() + val horizontalGridColumns by context.symphony.settings.lastUsedArtistsHorizontalGridColumns.flow.collectAsStateWithLifecycle() + val verticalGridColumns by context.symphony.settings.lastUsedArtistsVerticalGridColumns.flow.collectAsStateWithLifecycle() val gridColumns by remember(horizontalGridColumns, verticalGridColumns) { derivedStateOf { ResponsiveGridColumns(horizontalGridColumns, verticalGridColumns) diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenericGrooveBanner.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenericGrooveBanner.kt index 61dd3a34..dd704558 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenericGrooveBanner.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenericGrooveBanner.kt @@ -1,8 +1,10 @@ package io.github.zyrouge.symphony.ui.components +import android.net.Uri import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.BoxWithConstraintsScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -27,30 +29,20 @@ import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import coil.compose.AsyncImage -import coil.request.ImageRequest +import io.github.zyrouge.symphony.ui.helpers.Assets import io.github.zyrouge.symphony.ui.helpers.ScreenOrientation +import io.github.zyrouge.symphony.ui.helpers.ViewContext +import io.github.zyrouge.symphony.ui.helpers.createHandyImageRequest @Composable fun GenericGrooveBanner( - image: ImageRequest, + image: @Composable (BoxWithConstraintsScope) -> Unit, options: @Composable (Boolean, () -> Unit) -> Unit, content: @Composable () -> Unit, ) { val defaultHorizontalPadding = 20.dp BoxWithConstraints { - AsyncImage( - image, - null, - contentScale = ContentScale.Crop, - modifier = Modifier - .fillMaxWidth() - .height( - when (ScreenOrientation.fromConfiguration(LocalConfiguration.current)) { - ScreenOrientation.PORTRAIT -> this@BoxWithConstraints.maxWidth.times(0.7f) - ScreenOrientation.LANDSCAPE -> this@BoxWithConstraints.maxWidth.times(0.25f) - } - ) - ) + image(this@BoxWithConstraints) Row( modifier = Modifier .background( @@ -94,3 +86,54 @@ fun GenericGrooveBanner( } } } + +@Composable +fun GenericGrooveBannerSingleImage( + context: ViewContext, + uri: Uri?, + constraints: BoxWithConstraintsScope, +) { + AsyncImage( + createGrooveImageRequest(context, uri), + null, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxWidth() + .height( + when (ScreenOrientation.fromConfiguration(LocalConfiguration.current)) { + ScreenOrientation.PORTRAIT -> constraints.maxWidth.times(0.7f) + ScreenOrientation.LANDSCAPE -> constraints.maxWidth.times(0.25f) + } + ) + ) +} + +@Composable +fun GenericGrooveBannerQuadImage( + context: ViewContext, + images: List, + constraints: BoxWithConstraintsScope, +) { + // TODO: implement collage + AsyncImage( + createGrooveImageRequest(context, images.firstOrNull()), + null, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxWidth() + .height( + when (ScreenOrientation.fromConfiguration(LocalConfiguration.current)) { + ScreenOrientation.PORTRAIT -> constraints.maxWidth.times(0.7f) + ScreenOrientation.LANDSCAPE -> constraints.maxWidth.times(0.25f) + } + ) + ) +} + +private fun createGrooveImageRequest(context: ViewContext, uri: Uri?) { + createHandyImageRequest( + context.symphony.applicationContext, + uri ?: Assets.getPlaceholderUri(context.symphony), + Assets.getPlaceholderId(context.symphony), + ) +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenericSongListDropdown.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenericSongListDropdown.kt index f269f1b8..5547a132 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenericSongListDropdown.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenericSongListDropdown.kt @@ -36,7 +36,7 @@ fun GenericSongListDropdown( }, onClick = { onDismissRequest() - context.symphony.radio.shorty.playQueue(songIds, shuffle = true) + context.symphony.radio.shorty.playQueue(songs, shuffle = true) } ) DropdownMenuItem( @@ -49,7 +49,7 @@ fun GenericSongListDropdown( onClick = { onDismissRequest() context.symphony.radio.queue.add( - songIds, + songs, context.symphony.radio.queue.currentSongIndex + 1 ) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenreGrid.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenreGrid.kt index d7e99a51..5911c6cc 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenreGrid.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenreGrid.kt @@ -20,7 +20,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -71,8 +70,8 @@ fun GenreGrid( sortBy: GenreRepository.SortBy, sortReverse: Boolean, ) { - val horizontalGridColumns by context.symphony.settings.lastUsedGenresHorizontalGridColumns.flow.collectAsState() - val verticalGridColumns by context.symphony.settings.lastUsedGenresVerticalGridColumns.flow.collectAsState() + val horizontalGridColumns by context.symphony.settings.lastUsedGenresHorizontalGridColumns.flow.collectAsStateWithLifecycle() + val verticalGridColumns by context.symphony.settings.lastUsedGenresVerticalGridColumns.flow.collectAsStateWithLifecycle() val gridColumns by remember(horizontalGridColumns, verticalGridColumns) { derivedStateOf { ResponsiveGridColumns(horizontalGridColumns, verticalGridColumns) @@ -171,7 +170,7 @@ private fun GenreTile( ), colors = GenreTile.cardColors(index), onClick = { - context.navController.navigate(GenreViewRoute(attributedGenre.genre.name)) + context.navController.navigate(GenreViewRoute(attributedGenre.entity.name)) } ) { Box( @@ -189,7 +188,7 @@ private fun GenreTile( .absoluteOffset(8.dp, 12.dp) ) { Text( - attributedGenre.genre.name, + attributedGenre.entity.name, textAlign = TextAlign.Start, style = MaterialTheme.typography.displaySmall .copy(fontWeight = FontWeight.Bold), @@ -203,7 +202,7 @@ private fun GenreTile( verticalArrangement = Arrangement.Center, ) { Text( - attributedGenre.genre.name, + attributedGenre.entity.name, textAlign = TextAlign.Center, style = MaterialTheme.typography.bodyLarge .copy(fontWeight = FontWeight.Bold), diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/IntroductoryDialog.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/IntroductoryDialog.kt index 2745bd17..e1343216 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/IntroductoryDialog.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/IntroductoryDialog.kt @@ -22,7 +22,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -35,8 +34,8 @@ fun IntroductoryDialog( context: ViewContext, onDismissRequest: () -> Unit, ) { - val checkForUpdates by context.symphony.settings.checkForUpdates.flow.collectAsState() - val showUpdateToast by context.symphony.settings.showUpdateToast.flow.collectAsState() + val checkForUpdates by context.symphony.settings.checkForUpdates.flow.collectAsStateWithLifecycle() + val showUpdateToast by context.symphony.settings.showUpdateToast.flow.collectAsStateWithLifecycle() ScaffoldDialog( onDismissRequest = onDismissRequest, diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/LyricsText.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/LyricsText.kt index 64be88ca..fd9096a1 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/LyricsText.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/LyricsText.kt @@ -9,7 +9,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf @@ -45,8 +44,8 @@ fun LyricsText( ) } var playbackPositionTimer: Timer? = remember { null } - val queue by context.symphony.radio.observatory.queue.collectAsState() - val queueIndex by context.symphony.radio.observatory.queueIndex.collectAsState() + val queue by context.symphony.radio.observatory.queue.collectAsStateWithLifecycle() + val queueIndex by context.symphony.radio.observatory.queueIndex.collectAsStateWithLifecycle() val song by remember(queue, queueIndex) { derivedStateOf { queue.getOrNull(queueIndex)?.let { context.symphony.groove.song.get(it) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/NewPlaylistDialog.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/NewPlaylistDialog.kt index 4a4d6f4c..d7abebde 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/NewPlaylistDialog.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/NewPlaylistDialog.kt @@ -22,14 +22,15 @@ import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp -import io.github.zyrouge.symphony.services.groove.Playlist +import io.github.zyrouge.symphony.services.groove.entities.Playlist +import io.github.zyrouge.symphony.services.groove.repositories.PlaylistRepository import io.github.zyrouge.symphony.ui.helpers.ViewContext @Composable fun NewPlaylistDialog( context: ViewContext, initialSongIds: List = listOf(), - onDone: (Playlist) -> Unit, + onDone: (PlaylistRepository.AddOptions) -> Unit, onDismissRequest: () -> Unit, ) { var input by remember { mutableStateOf("") } @@ -81,11 +82,17 @@ fun NewPlaylistDialog( TextButton( enabled = input.isNotBlank(), onClick = { - val playlist = context.symphony.groove.playlist.create( + val playlist = Playlist( + id = context.symphony.database.playlistsIdGenerator.next(), title = input, + uri = null, + path = null, + ) + val addOptions = PlaylistRepository.AddOptions( + playlist = playlist, songIds = songIds.toList(), ) - onDone(playlist) + onDone(addOptions) } ) { Text(context.symphony.t.Done) diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/NowPlayingBottomBar.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/NowPlayingBottomBar.kt index fd89aee3..ae363a91 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/NowPlayingBottomBar.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/NowPlayingBottomBar.kt @@ -40,7 +40,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf @@ -97,19 +96,19 @@ private fun nowPlayingBottomBarEnterAnimationSpec() = TransitionDurations.No @Composable fun NowPlayingBottomBar(context: ViewContext, insetPadding: Boolean = true) { - val queue by context.symphony.radio.observatory.queue.collectAsState() - val queueIndex by context.symphony.radio.observatory.queueIndex.collectAsState() + val queue by context.symphony.radio.observatory.queue.collectAsStateWithLifecycle() + val queueIndex by context.symphony.radio.observatory.queueIndex.collectAsStateWithLifecycle() val currentPlayingSong by remember(queue, queueIndex) { derivedStateOf { queue.getOrNull(queueIndex)?.let { context.symphony.groove.song.get(it) } } } - val isPlaying by context.symphony.radio.observatory.isPlaying.collectAsState() - val playbackPosition by context.symphony.radio.observatory.playbackPosition.collectAsState() - val showTrackControls by context.symphony.settings.miniPlayerTrackControls.flow.collectAsState() - val showSeekControls by context.symphony.settings.miniPlayerSeekControls.flow.collectAsState() - val seekBackDuration by context.symphony.settings.seekBackDuration.flow.collectAsState() - val seekForwardDuration by context.symphony.settings.seekForwardDuration.flow.collectAsState() + val isPlaying by context.symphony.radio.observatory.isPlaying.collectAsStateWithLifecycle() + val playbackPosition by context.symphony.radio.observatory.playbackPosition.collectAsStateWithLifecycle() + val showTrackControls by context.symphony.settings.miniPlayerTrackControls.flow.collectAsStateWithLifecycle() + val showSeekControls by context.symphony.settings.miniPlayerSeekControls.flow.collectAsStateWithLifecycle() + val seekBackDuration by context.symphony.settings.seekBackDuration.flow.collectAsStateWithLifecycle() + val seekForwardDuration by context.symphony.settings.seekForwardDuration.flow.collectAsStateWithLifecycle() AnimatedContent( modifier = Modifier.fillMaxWidth(), @@ -320,7 +319,7 @@ private fun NowPlayingBottomBarContentText( text: String, style: TextStyle, ) { - val textMarquee by context.symphony.settings.miniPlayerTextMarquee.flow.collectAsState() + val textMarquee by context.symphony.settings.miniPlayerTextMarquee.flow.collectAsStateWithLifecycle() var showOverlay by remember { mutableStateOf(false) } Box { diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/PlaylistGrid.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/PlaylistGrid.kt index 5721d58c..a94b994e 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/PlaylistGrid.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/PlaylistGrid.kt @@ -8,7 +8,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -28,8 +27,8 @@ fun PlaylistGrid( sortReverse: Boolean, leadingContent: @Composable () -> Unit = {}, ) { - val horizontalGridColumns by context.symphony.settings.lastUsedPlaylistsHorizontalGridColumns.flow.collectAsState() - val verticalGridColumns by context.symphony.settings.lastUsedPlaylistsVerticalGridColumns.flow.collectAsState() + val horizontalGridColumns by context.symphony.settings.lastUsedPlaylistsHorizontalGridColumns.flow.collectAsStateWithLifecycle() + val verticalGridColumns by context.symphony.settings.lastUsedPlaylistsVerticalGridColumns.flow.collectAsStateWithLifecycle() val gridColumns by remember(horizontalGridColumns, verticalGridColumns) { derivedStateOf { ResponsiveGridColumns(horizontalGridColumns, verticalGridColumns) diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/PlaylistManageSongsDialog.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/PlaylistManageSongsDialog.kt index 98ee24b6..c7e7c978 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/PlaylistManageSongsDialog.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/PlaylistManageSongsDialog.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check @@ -22,7 +21,6 @@ import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -33,18 +31,22 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.unit.dp +import io.github.zyrouge.symphony.services.groove.entities.Song import io.github.zyrouge.symphony.ui.helpers.ViewContext @Composable fun PlaylistManageSongsDialog( context: ViewContext, - selectedSongIds: List, - onDone: (List) -> Unit, + selectedSongs: List, + onDone: (List) -> Unit, ) { - val allSongIds by context.symphony.groove.song.all.collectAsState() - val nSelectedSongIds = remember { selectedSongIds.toMutableStateList() } + val songsSortBy by context.symphony.settings.lastUsedSongsSortBy.flow.collectAsStateWithLifecycle() + val songsSortReverse by context.symphony.settings.lastUsedSongsSortReverse.flow.collectAsStateWithLifecycle() + val allSongs by context.symphony.groove.song.valuesAsFlow(songsSortBy, songsSortReverse) + .collectAsStateWithLifecycle(emptyList()) + val nSelectedSongs = remember { selectedSongs.toMutableStateList() } var terms by remember { mutableStateOf("") } - val songIds by remember(allSongIds, terms, selectedSongIds) { + val songIds by remember(allSongs, terms, selectedSongs) { derivedStateOf { context.symphony.groove.song.search(allSongIds, terms, limit = -1) .map { it.entity } @@ -54,7 +56,7 @@ fun PlaylistManageSongsDialog( ScaffoldDialog( onDismissRequest = { - onDone(nSelectedSongIds.toList()) + onDone(nSelectedSongs.toList()) }, title = { Text(context.symphony.t.ManageSongs) @@ -65,7 +67,7 @@ fun PlaylistManageSongsDialog( .padding(start = 8.dp) .clip(CircleShape) .clickable { - onDone(selectedSongIds) + onDone(nSelectedSongs.toList()) }, ) { Icon( @@ -81,7 +83,7 @@ fun PlaylistManageSongsDialog( .padding(end = 8.dp) .clip(CircleShape) .clickable { - onDone(nSelectedSongIds.toList()) + onDone(nSelectedSongs.toList()) }, ) { Icon( @@ -112,7 +114,7 @@ fun PlaylistManageSongsDialog( }, ) when { - songIds.isEmpty() -> Box(modifier = Modifier.padding(0.dp, 12.dp)) { + allSongs.isEmpty() -> Box(modifier = Modifier.padding(0.dp, 12.dp)) { SubtleCaptionText(context.symphony.t.DamnThisIsSoEmpty) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/PlaylistTile.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/PlaylistTile.kt index 1d6ff981..d3c29799 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/PlaylistTile.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/PlaylistTile.kt @@ -32,7 +32,6 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -46,7 +45,8 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import io.github.zyrouge.symphony.services.groove.MediaExposer -import io.github.zyrouge.symphony.services.groove.Playlist +import io.github.zyrouge.symphony.services.groove.entities.Playlist +import io.github.zyrouge.symphony.services.groove.entities.Song import io.github.zyrouge.symphony.ui.helpers.ViewContext import io.github.zyrouge.symphony.ui.theme.ThemeColors import io.github.zyrouge.symphony.ui.view.PlaylistViewRoute @@ -54,7 +54,8 @@ import io.github.zyrouge.symphony.utils.Logger @Composable fun PlaylistTile(context: ViewContext, playlist: Playlist) { - val updateId by context.symphony.groove.playlist.updateId.collectAsState() + val artworks by context.symphony.groove.playlist.getTop4ArtworkUriAsFlow(playlist.id) + .collectAsStateWithLifecycle(emptyList()) Card( modifier = Modifier @@ -70,9 +71,7 @@ fun PlaylistTile(context: ViewContext, playlist: Playlist) { Box { AsyncImage( // TODO: remove this hack after moving to reactive objects - remember(updateId, playlist) { - playlist.createArtworkImageRequest(context.symphony).build() - }, + artworks.first(), null, contentScale = ContentScale.Crop, modifier = Modifier @@ -86,6 +85,7 @@ fun PlaylistTile(context: ViewContext, playlist: Playlist) { .padding(top = 4.dp) ) { var showOptionsMenu by remember { mutableStateOf(false) } + IconButton( onClick = { showOptionsMenu = !showOptionsMenu } ) { @@ -136,11 +136,10 @@ fun PlaylistTile(context: ViewContext, playlist: Playlist) { @Composable fun PlaylistDropdownMenu( context: ViewContext, - playlist: Playlist, + playlist: Playlist.AlongAttributes, + songs: List, expanded: Boolean, - onSongsChanged: (() -> Unit) = {}, - onRename: (() -> Unit) = {}, - onDelete: (() -> Unit) = {}, + onDelete: () -> Unit, onDismissRequest: () -> Unit, ) { val savePlaylistLauncher = rememberLauncherForActivityResult( @@ -186,10 +185,7 @@ fun PlaylistDropdownMenu( }, onClick = { onDismissRequest() - context.symphony.radio.shorty.playQueue( - playlist.getSortedSongIds(context.symphony), - shuffle = true, - ) + context.symphony.radio.shorty.playQueue(songs, shuffle = true) } ) DropdownMenuItem( @@ -202,7 +198,7 @@ fun PlaylistDropdownMenu( onClick = { onDismissRequest() context.symphony.radio.queue.add( - playlist.getSortedSongIds(context.symphony), + songs, context.symphony.radio.queue.currentSongIndex + 1 ) } @@ -219,7 +215,7 @@ fun PlaylistDropdownMenu( showAddToPlaylistDialog = true } ) - if (playlist.isNotLocal) { + if (!playlist.entity.isModifiable) { DropdownMenuItem( leadingIcon = { Icon(Icons.AutoMirrored.Filled.PlaylistAdd, null) @@ -245,7 +241,7 @@ fun PlaylistDropdownMenu( showInfoDialog = true } ) - if (playlist.isNotLocal) { + if (playlist.entity.isModifiable) { DropdownMenuItem( leadingIcon = { Icon(Icons.Filled.Save, null) @@ -256,7 +252,7 @@ fun PlaylistDropdownMenu( onClick = { onDismissRequest() try { - savePlaylistLauncher.launch("${playlist.title}.m3u") + savePlaylistLauncher.launch("${playlist.entity.title}.m3u") } catch (err: Exception) { Logger.error("PlaylistTile", "export failed", err) Toast.makeText( @@ -282,7 +278,7 @@ fun PlaylistDropdownMenu( } ) } - if (!context.symphony.groove.playlist.isBuiltInPlaylist(playlist)) { + if (!playlist.entity.isInternal) { DropdownMenuItem( leadingIcon = { Icon( @@ -318,7 +314,6 @@ fun PlaylistDropdownMenu( selectedSongIds = playlist.getSongIds(context.symphony), onDone = { context.symphony.groove.playlist.update(playlist.id, it) - onSongsChanged() showSongsPicker = false } ) diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongCard.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongCard.kt index cbea674a..84b9b7aa 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongCard.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongCard.kt @@ -35,7 +35,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -47,8 +46,9 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.compose.AsyncImage -import io.github.zyrouge.symphony.services.groove.Song +import io.github.zyrouge.symphony.services.groove.entities.Song import io.github.zyrouge.symphony.ui.helpers.ViewContext import io.github.zyrouge.symphony.ui.view.AlbumArtistViewRoute import io.github.zyrouge.symphony.ui.view.AlbumViewRoute @@ -68,15 +68,15 @@ fun SongCard( trailingOptionsContent: (@Composable ColumnScope.(() -> Unit) -> Unit)? = null, onClick: () -> Unit, ) { - val queue by context.symphony.radio.observatory.queue.collectAsState() - val queueIndex by context.symphony.radio.observatory.queueIndex.collectAsState() + val queue by context.symphony.radio.observatory.queue.collectAsStateWithLifecycle() + val queueIndex by context.symphony.radio.observatory.queueIndex.collectAsStateWithLifecycle() val isCurrentPlaying by remember(autoHighlight, song, queue) { derivedStateOf { autoHighlight && song.id == queue.getOrNull(queueIndex) } } - val favoriteSongIds by context.symphony.groove.playlist.favorites.collectAsState() - val isFavorite by remember(favoriteSongIds, song) { - derivedStateOf { favoriteSongIds.contains(song.id) } - } + val isFavorite by context.symphony.groove.playlist.isFavoriteSongAsFlow(song.id) + .collectAsStateWithLifecycle(false) + val artwork by context.symphony.groove.song.getArtworkUriAsFlow(song.id) + .collectAsStateWithLifecycle(null) Card( modifier = Modifier.fillMaxWidth(), diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongExplorerList.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongExplorerList.kt index dd99b4df..cc6408b5 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongExplorerList.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongExplorerList.kt @@ -31,7 +31,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -74,8 +73,8 @@ fun SongExplorerList( initialPath?.let { explorer.navigateToFolder(it) } ?: explorer ) } - val sortBy by context.symphony.settings.lastUsedBrowserSortBy.flow.collectAsState() - val sortReverse by context.symphony.settings.lastUsedBrowserSortReverse.flow.collectAsState() + val sortBy by context.symphony.settings.lastUsedBrowserSortBy.flow.collectAsStateWithLifecycle() + val sortReverse by context.symphony.settings.lastUsedBrowserSortReverse.flow.collectAsStateWithLifecycle() val sortedEntities by remember(key, currentFolder) { derivedStateOf { val categorized = currentFolder.categorizedChildren() diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongTreeList.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongTreeList.kt index 7e8a24af..fe3ef2dc 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongTreeList.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongTreeList.kt @@ -41,7 +41,6 @@ import androidx.compose.material3.RadioButton import androidx.compose.material3.Text import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf @@ -77,10 +76,10 @@ fun SongTreeList( addAll(initialDisabled) } } - val pathsSortBy by context.symphony.settings.lastUsedTreePathSortBy.flow.collectAsState() - val pathsSortReverse by context.symphony.settings.lastUsedTreePathSortReverse.flow.collectAsState() - val songsSortBy by context.symphony.settings.lastUsedSongsSortBy.flow.collectAsState() - val songsSortReverse by context.symphony.settings.lastUsedSongsSortReverse.flow.collectAsState() + val pathsSortBy by context.symphony.settings.lastUsedTreePathSortBy.flow.collectAsStateWithLifecycle() + val pathsSortReverse by context.symphony.settings.lastUsedTreePathSortReverse.flow.collectAsStateWithLifecycle() + val songsSortBy by context.symphony.settings.lastUsedSongsSortBy.flow.collectAsStateWithLifecycle() + val songsSortReverse by context.symphony.settings.lastUsedSongsSortReverse.flow.collectAsStateWithLifecycle() val sortedTree by remember(tree, pathsSortBy, pathsSortReverse, songsSortBy, songsSortReverse) { derivedStateOf { val pairs = StringListUtils.sort(tree.keys.toList(), pathsSortBy, pathsSortReverse) @@ -162,12 +161,12 @@ fun SongTreeListContent( togglePath: (String) -> Unit, ) { val lazyListState = rememberLazyListState() - val queue by context.symphony.radio.observatory.queue.collectAsState() - val queueIndex by context.symphony.radio.observatory.queueIndex.collectAsState() + val queue by context.symphony.radio.observatory.queue.collectAsStateWithLifecycle() + val queueIndex by context.symphony.radio.observatory.queueIndex.collectAsStateWithLifecycle() val currentPlayingSongId by remember(queue, queueIndex) { derivedStateOf { queue.getOrNull(queueIndex) } } - val favoriteIds by context.symphony.groove.playlist.favorites.collectAsState() + val favoriteIds by context.symphony.groove.playlist.favorites.collectAsStateWithLifecycle() LazyColumn( state = lazyListState, diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/theme/Theme.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/theme/Theme.kt index 45684468..d42de0e5 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/theme/Theme.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/theme/Theme.kt @@ -10,7 +10,6 @@ import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity @@ -40,12 +39,12 @@ fun SymphonyTheme( context: ViewContext, content: @Composable () -> Unit, ) { - val themeMode by context.symphony.settings.themeMode.flow.collectAsState() - val useMaterialYou by context.symphony.settings.useMaterialYou.flow.collectAsState() - val primaryColorName by context.symphony.settings.primaryColor.flow.collectAsState() - val fontName by context.symphony.settings.fontFamily.flow.collectAsState() - val fontScale by context.symphony.settings.fontScale.flow.collectAsState() - val contentScale by context.symphony.settings.contentScale.flow.collectAsState() + val themeMode by context.symphony.settings.themeMode.flow.collectAsStateWithLifecycle() + val useMaterialYou by context.symphony.settings.useMaterialYou.flow.collectAsStateWithLifecycle() + val primaryColorName by context.symphony.settings.primaryColor.flow.collectAsStateWithLifecycle() + val fontName by context.symphony.settings.fontFamily.flow.collectAsStateWithLifecycle() + val fontScale by context.symphony.settings.fontScale.flow.collectAsStateWithLifecycle() + val contentScale by context.symphony.settings.contentScale.flow.collectAsStateWithLifecycle() val colorSchemeMode = themeMode.toColorSchemeMode(isSystemInDarkTheme()) val colorScheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && useMaterialYou) { diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Album.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Album.kt index 85090226..d744a323 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Album.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Album.kt @@ -26,7 +26,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -36,18 +35,22 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import io.github.zyrouge.symphony.services.groove.entities.Album +import io.github.zyrouge.symphony.services.groove.entities.Artist import io.github.zyrouge.symphony.ui.components.AlbumDropdownMenu import io.github.zyrouge.symphony.ui.components.AnimatedNowPlayingBottomBar import io.github.zyrouge.symphony.ui.components.GenericGrooveBanner +import io.github.zyrouge.symphony.ui.components.GenericGrooveBannerQuadImage import io.github.zyrouge.symphony.ui.components.IconButtonPlaceholder import io.github.zyrouge.symphony.ui.components.IconTextBody import io.github.zyrouge.symphony.ui.components.SongCardThumbnailLabelStyle import io.github.zyrouge.symphony.ui.components.SongList import io.github.zyrouge.symphony.ui.components.TopAppBarMinimalTitle import io.github.zyrouge.symphony.ui.helpers.ViewContext +import io.github.zyrouge.symphony.utils.DurationUtils import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.transformLatest import kotlinx.serialization.Serializable @@ -58,22 +61,28 @@ data class AlbumViewRoute(val albumId: String) @Composable fun AlbumView(context: ViewContext, route: AlbumViewRoute) { val albumFlow = context.symphony.groove.album.findByIdAsFlow(route.albumId) - val album by albumFlow.collectAsState(null) - val songsSortBy by context.symphony.settings.lastUsedAlbumSongsSortBy.flow.collectAsState() - val songsSortReverse by context.symphony.settings.lastUsedAlbumSongsSortReverse.flow.collectAsState() - val songs by albumFlow - .transformLatest { album -> - val value = when { - album == null -> emptyFlow() - else -> context.symphony.groove.album.findSongsByIdAsFlow( - album.id, - songsSortBy, - songsSortReverse, - ) - } - emitAll(value) + val album by albumFlow.collectAsStateWithLifecycle(null) + val artists by context.symphony.groove.album.findArtistsOfIdAsFlow(route.albumId) + .collectAsStateWithLifecycle(emptyList()) + val songsSortBy by context.symphony.settings.lastUsedAlbumSongsSortBy.flow.collectAsStateWithLifecycle() + val songsSortReverse by context.symphony.settings.lastUsedAlbumSongsSortReverse.flow.collectAsStateWithLifecycle() + val songsFlow = albumFlow.transformLatest { album -> + val value = when { + album == null -> emptyFlow() + else -> context.symphony.groove.album.findSongsByIdAsFlow( + album.entity.id, + songsSortBy, + songsSortReverse, + ) + } + emitAll(value) + } + val songs by songsFlow.collectAsStateWithLifecycle(emptyList()) + val duration by songsFlow + .mapLatest { + it.fold(0L) { target, x -> target + x.duration } } - .collectAsState(emptyList()) + .collectAsStateWithLifecycle(0L) Scaffold( modifier = Modifier.fillMaxSize(), @@ -87,7 +96,8 @@ fun AlbumView(context: ViewContext, route: AlbumViewRoute) { title = { TopAppBarMinimalTitle { Text( - context.symphony.t.Album + (album?.let { " - ${it.name}" } ?: ""), + context.symphony.t.Album + (album?.let { " - ${it.entity.name}" } + ?: ""), maxLines = 2, overflow = TextOverflow.Ellipsis, ) @@ -115,7 +125,12 @@ fun AlbumView(context: ViewContext, route: AlbumViewRoute) { sortReverse = songsSortReverse, leadingContent = { item { - AlbumHero(context, album!!) + AlbumHero( + context, + album = album!!, + artists = artists, + duration = duration, + ) } }, cardThumbnailLabel = { _, song -> @@ -136,9 +151,19 @@ fun AlbumView(context: ViewContext, route: AlbumViewRoute) { @OptIn(ExperimentalLayoutApi::class) @Composable -private fun AlbumHero(context: ViewContext, album: Album.AlongAttributes) { +private fun AlbumHero( + context: ViewContext, + album: Album.AlongAttributes, + artists: List, + duration: Long, +) { + val artworks by context.symphony.groove.album.getTop4ArtworkUriAsFlow(album.entity.id) + .collectAsStateWithLifecycle(emptyList()) + GenericGrooveBanner( - image = album.createArtworkImageRequest(context.symphony).build(), + image = { constraints -> + GenericGrooveBannerQuadImage(context, artworks, constraints) + }, options = { expanded, onDismissRequest -> AlbumDropdownMenu( context, @@ -149,22 +174,22 @@ private fun AlbumHero(context: ViewContext, album: Album.AlongAttributes) { }, content = { Column { - Text(album.name) - if (album.artists.isNotEmpty()) { + Text(album.entity.name) + if (artists.isNotEmpty()) { ProvideTextStyle(MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Bold)) { FlowRow { - album.artists.forEachIndexed { i, it -> + artists.forEachIndexed { i, it -> Text( - it, + it.entity.name, maxLines = 2, overflow = TextOverflow.Ellipsis, modifier = Modifier.pointerInput(Unit) { detectTapGestures { _ -> - context.navController.navigate(ArtistViewRoute(it)) + context.navController.navigate(ArtistViewRoute(it.entity.id)) } }, ) - if (i != album.artists.size - 1) { + if (i != artists.size - 1) { Text(", ") } } @@ -176,8 +201,8 @@ private fun AlbumHero(context: ViewContext, album: Album.AlongAttributes) { horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically, ) { - album.startYear?.let { startYear -> - val endYear = album.endYear + album.entity.startYear?.let { startYear -> + val endYear = album.entity.endYear Text( when { @@ -189,7 +214,7 @@ private fun AlbumHero(context: ViewContext, album: Album.AlongAttributes) { CircleSeparator() } Text( - album.duration.toString(), + DurationUtils.formatMs(duration), style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Bold) ) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/AlbumArtist.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/AlbumArtist.kt deleted file mode 100644 index 4ed1a55d..00000000 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/AlbumArtist.kt +++ /dev/null @@ -1,156 +0,0 @@ -package io.github.zyrouge.symphony.ui.view - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.PriorityHigh -import androidx.compose.material3.CenterAlignedTopAppBar -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import io.github.zyrouge.symphony.services.groove.AlbumArtist -import io.github.zyrouge.symphony.ui.components.AlbumArtistDropdownMenu -import io.github.zyrouge.symphony.ui.components.AlbumRow -import io.github.zyrouge.symphony.ui.components.AnimatedNowPlayingBottomBar -import io.github.zyrouge.symphony.ui.components.GenericGrooveBanner -import io.github.zyrouge.symphony.ui.components.IconButtonPlaceholder -import io.github.zyrouge.symphony.ui.components.IconTextBody -import io.github.zyrouge.symphony.ui.components.SongList -import io.github.zyrouge.symphony.ui.components.TopAppBarMinimalTitle -import io.github.zyrouge.symphony.ui.helpers.ViewContext -import kotlinx.serialization.Serializable - -@Serializable -data class AlbumArtistViewRoute(val albumArtistName: String) - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun AlbumArtistView(context: ViewContext, route: AlbumArtistViewRoute) { - val allAlbumArtistNames by context.symphony.groove.albumArtist.all.collectAsState() - val allSongIds by context.symphony.groove.song.all.collectAsState() - val allAlbumIds = context.symphony.groove.album.all - val albumArtist by remember(allAlbumArtistNames) { - derivedStateOf { context.symphony.groove.albumArtist.get(route.albumArtistName) } - } - val songIds by remember(albumArtist, allSongIds) { - derivedStateOf { albumArtist?.getSongIds(context.symphony) ?: listOf() } - } - val albumIds by remember(albumArtist, allAlbumIds) { - derivedStateOf { albumArtist?.getAlbumIds(context.symphony) ?: listOf() } - } - val isViable by remember(albumArtist) { - derivedStateOf { albumArtist != null } - } - - Scaffold( - modifier = Modifier.fillMaxSize(), - topBar = { - CenterAlignedTopAppBar( - navigationIcon = { - IconButton( - onClick = { context.navController.popBackStack() } - ) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, null) - } - }, - title = { - TopAppBarMinimalTitle { - Text( - context.symphony.t.AlbumArtist + - (albumArtist?.let { " - ${it.name}" } ?: ""), - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) - } - }, - colors = TopAppBarDefaults.centerAlignedTopAppBarColors( - containerColor = Color.Transparent - ), - actions = { - IconButtonPlaceholder() - }, - ) - }, - content = { contentPadding -> - Box( - modifier = Modifier - .padding(contentPadding) - .fillMaxSize() - ) { - if (isViable) { - SongList( - context, - songIds = songIds, - leadingContent = { - item { - AlbumArtistHero(context, albumArtist!!) - } - if (albumIds.isNotEmpty()) { - item { - Spacer(modifier = Modifier.height(4.dp)) - AlbumRow(context, albumIds) - Spacer(modifier = Modifier.height(4.dp)) - HorizontalDivider() - } - } - } - ) - } else UnknownAlbumArtist(context, route.albumArtistName) - } - }, - bottomBar = { - AnimatedNowPlayingBottomBar(context) - } - ) -} - -@Composable -private fun AlbumArtistHero(context: ViewContext, albumArtist: AlbumArtist) { - GenericGrooveBanner( - image = albumArtist.createArtworkImageRequest(context.symphony).build(), - options = { expanded, onDismissRequest -> - AlbumArtistDropdownMenu( - context, - albumArtist, - expanded = expanded, - onDismissRequest = onDismissRequest - ) - }, - content = { - Text(albumArtist.name) - } - ) -} - -@Composable -private fun UnknownAlbumArtist(context: ViewContext, artistName: String) { - IconTextBody( - icon = { modifier -> - Icon( - Icons.Filled.PriorityHigh, - null, - modifier = modifier - ) - }, - content = { - Text(context.symphony.t.UnknownArtistX(artistName)) - } - ) -} diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Artist.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Artist.kt index 423057ef..a098689c 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Artist.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Artist.kt @@ -17,47 +17,52 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import io.github.zyrouge.symphony.services.groove.Artist +import io.github.zyrouge.symphony.services.groove.entities.Artist import io.github.zyrouge.symphony.ui.components.AlbumRow import io.github.zyrouge.symphony.ui.components.AnimatedNowPlayingBottomBar import io.github.zyrouge.symphony.ui.components.ArtistDropdownMenu import io.github.zyrouge.symphony.ui.components.GenericGrooveBanner +import io.github.zyrouge.symphony.ui.components.GenericGrooveBannerQuadImage import io.github.zyrouge.symphony.ui.components.IconButtonPlaceholder import io.github.zyrouge.symphony.ui.components.IconTextBody import io.github.zyrouge.symphony.ui.components.SongList import io.github.zyrouge.symphony.ui.components.TopAppBarMinimalTitle import io.github.zyrouge.symphony.ui.helpers.ViewContext +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.transformLatest import kotlinx.serialization.Serializable @Serializable -data class ArtistViewRoute(val artistName: String) +data class ArtistViewRoute(val artistId: String) -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalCoroutinesApi::class) @Composable fun ArtistView(context: ViewContext, route: ArtistViewRoute) { - val allArtistNames by context.symphony.groove.artist.all.collectAsState() - val allSongIds by context.symphony.groove.song.all.collectAsState() - val allAlbumIds by context.symphony.groove.album.all.collectAsState() - val artist by remember(allArtistNames) { - derivedStateOf { context.symphony.groove.artist.get(route.artistName) } - } - val songIds by remember(artist, allSongIds) { - derivedStateOf { artist?.getSongIds(context.symphony) ?: listOf() } - } - val albumIds by remember(artist, allAlbumIds) { - derivedStateOf { artist?.getAlbumIds(context.symphony) ?: listOf() } - } - val isViable by remember(allArtistNames) { - derivedStateOf { allArtistNames.contains(route.artistName) } + val artistFlow = context.symphony.groove.artist.findByIdAsFlow(route.artistId) + val artist by artistFlow.collectAsStateWithLifecycle(null) + val albums by context.symphony.groove.artist.findAlbumsOfIdAsFlow(route.artistId) + .collectAsStateWithLifecycle(emptyList()) + val songsSortBy by context.symphony.settings.lastUsedArtistSongsSortBy.flow.collectAsStateWithLifecycle() + val songsSortReverse by context.symphony.settings.lastUsedArtistSongsSortReverse.flow.collectAsStateWithLifecycle() + val songsFlow = artistFlow.transformLatest { artist -> + val value = when { + artist == null -> emptyFlow() + else -> context.symphony.groove.artist.findSongsByIdAsFlow( + artist.entity.id, + songsSortBy, + songsSortReverse, + ) + } + emitAll(value) } + val songs by songsFlow.collectAsStateWithLifecycle(emptyList()) Scaffold( modifier = Modifier.fillMaxSize(), @@ -73,7 +78,7 @@ fun ArtistView(context: ViewContext, route: ArtistViewRoute) { title = { TopAppBarMinimalTitle { Text( - context.symphony.t.Artist + (artist?.let { " - ${it.name}" } ?: ""), + "${context.symphony.t.Artist} - ${artist?.entity?.name ?: context.symphony.t.UnknownSymbol}", maxLines = 2, overflow = TextOverflow.Ellipsis, ) @@ -93,25 +98,29 @@ fun ArtistView(context: ViewContext, route: ArtistViewRoute) { .padding(contentPadding) .fillMaxSize() ) { - if (isViable) { - SongList( + when { + artist != null -> SongList( context, - songIds = songIds, + songs = songs, + sortBy = songsSortBy, + sortReverse = songsSortReverse, leadingContent = { item { ArtistHero(context, artist!!) } - if (albumIds.isNotEmpty()) { + if (albums.isNotEmpty()) { item { Spacer(modifier = Modifier.height(4.dp)) - AlbumRow(context, albumIds) + AlbumRow(context, albums) Spacer(modifier = Modifier.height(4.dp)) HorizontalDivider() } } } ) - } else UnknownArtist(context, route.artistName) + + else -> UnknownArtist(context, route.artistId) + } } }, bottomBar = { @@ -121,9 +130,14 @@ fun ArtistView(context: ViewContext, route: ArtistViewRoute) { } @Composable -private fun ArtistHero(context: ViewContext, artist: Artist) { +private fun ArtistHero(context: ViewContext, artist: Artist.AlongAttributes) { + val artworks by context.symphony.groove.artist.getTop4ArtworkUriAsFlow(artist.entity.id) + .collectAsStateWithLifecycle(emptyList()) + GenericGrooveBanner( - image = artist.createArtworkImageRequest(context.symphony).build(), + image = { constraints -> + GenericGrooveBannerQuadImage(context, artworks, constraints) + }, options = { expanded, onDismissRequest -> ArtistDropdownMenu( context, @@ -133,13 +147,13 @@ private fun ArtistHero(context: ViewContext, artist: Artist) { ) }, content = { - Text(artist.name) + Text(artist.entity.name) } ) } @Composable -private fun UnknownArtist(context: ViewContext, artistName: String) { +private fun UnknownArtist(context: ViewContext, artistId: String) { IconTextBody( icon = { modifier -> Icon( @@ -149,7 +163,7 @@ private fun UnknownArtist(context: ViewContext, artistName: String) { ) }, content = { - Text(context.symphony.t.UnknownArtistX(artistName)) + Text(context.symphony.t.UnknownArtistX(artistId)) } ) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Base.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Base.kt index b005886d..84c679b1 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Base.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Base.kt @@ -71,9 +71,6 @@ fun BaseView(symphony: Symphony, activity: MainActivity) { baseComposable { SearchView(context, it.toRoute()) } - baseComposable { - AlbumArtistView(context, it.toRoute()) - } baseComposable { GenreView(context, it.toRoute()) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Genre.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Genre.kt index e3c485ef..4fcab52e 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Genre.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Genre.kt @@ -15,8 +15,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -29,25 +27,34 @@ import io.github.zyrouge.symphony.ui.components.IconTextBody import io.github.zyrouge.symphony.ui.components.SongList import io.github.zyrouge.symphony.ui.components.TopAppBarMinimalTitle import io.github.zyrouge.symphony.ui.helpers.ViewContext +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.transformLatest import kotlinx.serialization.Serializable @Serializable -data class GenreViewRoute(val genreName: String) +data class GenreViewRoute(val genreId: String) -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalCoroutinesApi::class) @Composable fun GenreView(context: ViewContext, route: GenreViewRoute) { - val allGenreNames by context.symphony.groove.genre.all.collectAsState() - val allSongIds by context.symphony.groove.song.all.collectAsState() - val genre by remember(allGenreNames) { - derivedStateOf { context.symphony.groove.genre.get(route.genreName) } - } - val songIds by remember(genre, allSongIds) { - derivedStateOf { genre?.getSongIds(context.symphony) ?: listOf() } - } - val isViable by remember(allGenreNames) { - derivedStateOf { allGenreNames.contains(route.genreName) } + val genreFlow = context.symphony.groove.genre.findByIdAsFlow(route.genreId) + val genre by genreFlow.collectAsStateWithLifecycle(null) + val songsSortBy by context.symphony.settings.lastUsedSongsSortBy.flow.collectAsStateWithLifecycle() + val songsSortReverse by context.symphony.settings.lastUsedSongsSortReverse.flow.collectAsStateWithLifecycle() + val songsFlow = genreFlow.transformLatest { genre -> + val value = when { + genre == null -> emptyFlow() + else -> context.symphony.groove.genre.findSongsByIdAsFlow( + genre.entity.id, + songsSortBy, + songsSortReverse, + ) + } + emitAll(value) } + val songs by songsFlow.collectAsStateWithLifecycle(emptyList()) Scaffold( modifier = Modifier.fillMaxSize(), @@ -62,8 +69,7 @@ fun GenreView(context: ViewContext, route: GenreViewRoute) { }, title = { TopAppBarMinimalTitle { - Text(context.symphony.t.Genre - + (genre?.let { " - ${it.name}" } ?: "")) + Text("${context.symphony.t.Genre} - ${genre?.entity?.name ?: context.symphony.t.UnknownSymbol}") } }, actions = { @@ -77,7 +83,7 @@ fun GenreView(context: ViewContext, route: GenreViewRoute) { Icon(Icons.Filled.MoreVert, null) GenericSongListDropdown( context, - songIds = songIds, + songIds = songs.map { it.id }, expanded = showOptionsMenu, onDismissRequest = { showOptionsMenu = false @@ -97,8 +103,14 @@ fun GenreView(context: ViewContext, route: GenreViewRoute) { .fillMaxSize() ) { when { - isViable -> SongList(context, songIds = songIds) - else -> UnknownGenre(context, route.genreName) + genre != null -> SongList( + context, + songs = songs, + sortBy = songsSortBy, + sortReverse = songsSortReverse, + ) + + else -> UnknownGenre(context, route.genreId) } } }, @@ -109,7 +121,7 @@ fun GenreView(context: ViewContext, route: GenreViewRoute) { } @Composable -private fun UnknownGenre(context: ViewContext, genre: String) { +private fun UnknownGenre(context: ViewContext, genreId: String) { IconTextBody( icon = { modifier -> Icon( @@ -119,7 +131,7 @@ private fun UnknownGenre(context: ViewContext, genre: String) { ) }, content = { - Text(context.symphony.t.UnknownGenreX(genre)) + Text(context.symphony.t.UnknownGenreX(genreId)) } ) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Home.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Home.kt index 7f07de37..0420a540 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Home.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Home.kt @@ -58,7 +58,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -168,10 +167,10 @@ object HomeViewRoute @OptIn(ExperimentalMaterial3Api::class) @Composable fun HomeView(context: ViewContext) { - val readIntroductoryMessage by context.symphony.settings.readIntroductoryMessage.flow.collectAsState() - val tabs by context.symphony.settings.homeTabs.flow.collectAsState() - val labelVisibility by context.symphony.settings.homePageBottomBarLabelVisibility.flow.collectAsState() - val currentTab by context.symphony.settings.lastHomeTab.flow.collectAsState() + val readIntroductoryMessage by context.symphony.settings.readIntroductoryMessage.flow.collectAsStateWithLifecycle() + val tabs by context.symphony.settings.homeTabs.flow.collectAsStateWithLifecycle() + val labelVisibility by context.symphony.settings.homePageBottomBarLabelVisibility.flow.collectAsStateWithLifecycle() + val currentTab by context.symphony.settings.lastHomeTab.flow.collectAsStateWithLifecycle() var showOptionsDropdown by remember { mutableStateOf(false) } var showTabsSheet by remember { mutableStateOf(false) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Lyrics.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Lyrics.kt index d2ed1434..a7c4917f 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Lyrics.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Lyrics.kt @@ -20,7 +20,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -44,7 +43,7 @@ object LyricsViewRoute @OptIn(ExperimentalMaterial3Api::class) @Composable fun LyricsView(context: ViewContext) { - val keepScreenAwake by context.symphony.settings.lyricsKeepScreenAwake.flow.collectAsState() + val keepScreenAwake by context.symphony.settings.lyricsKeepScreenAwake.flow.collectAsStateWithLifecycle() if (keepScreenAwake) { KeepScreenAwake() @@ -70,10 +69,7 @@ fun LyricsView(context: ViewContext) { }, title = { TopAppBarMinimalTitle { - Text( - context.symphony.t.Lyrics + - (data?.song?.title?.let { " - $it" } ?: "") - ) + Text("${context.symphony.t.Lyrics} - ${data?.song?.title ?: context.symphony.t.UnknownSymbol}") } }, actions = { diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/NowPlaying.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/NowPlaying.kt index 8646a476..3e92591c 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/NowPlaying.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/NowPlaying.kt @@ -2,11 +2,10 @@ package io.github.zyrouge.symphony.ui.view import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import io.github.zyrouge.symphony.services.groove.Song +import io.github.zyrouge.symphony.services.groove.entities.Song import io.github.zyrouge.symphony.services.radio.RadioQueue import io.github.zyrouge.symphony.ui.helpers.ViewContext import io.github.zyrouge.symphony.ui.view.nowPlaying.NothingPlaying @@ -73,8 +72,8 @@ fun NowPlayingObserver( context: ViewContext, content: @Composable (NowPlayingData?) -> Unit, ) { - val queue by context.symphony.radio.observatory.queue.collectAsState() - val queueIndex by context.symphony.radio.observatory.queueIndex.collectAsState() + val queue by context.symphony.radio.observatory.queue.collectAsStateWithLifecycle() + val queueIndex by context.symphony.radio.observatory.queueIndex.collectAsStateWithLifecycle() val song by remember(queue, queueIndex) { derivedStateOf { queue.getOrNull(queueIndex)?.let { context.symphony.groove.song.get(it) } @@ -84,21 +83,21 @@ fun NowPlayingObserver( derivedStateOf { song != null } } - val isPlaying by context.symphony.radio.observatory.isPlaying.collectAsState() - val currentLoopMode by context.symphony.radio.observatory.loopMode.collectAsState() - val currentShuffleMode by context.symphony.radio.observatory.shuffleMode.collectAsState() - val currentSpeed by context.symphony.radio.observatory.speed.collectAsState() - val currentPitch by context.symphony.radio.observatory.pitch.collectAsState() - val persistedSpeed by context.symphony.radio.observatory.persistedSpeed.collectAsState() - val persistedPitch by context.symphony.radio.observatory.persistedPitch.collectAsState() - val sleepTimer by context.symphony.radio.observatory.sleepTimer.collectAsState() - val pauseOnCurrentSongEnd by context.symphony.radio.observatory.pauseOnCurrentSongEnd.collectAsState() - val showSongAdditionalInfo by context.symphony.settings.nowPlayingAdditionalInfo.flow.collectAsState() - val enableSeekControls by context.symphony.settings.nowPlayingSeekControls.flow.collectAsState() - val seekBackDuration by context.symphony.settings.seekBackDuration.flow.collectAsState() - val seekForwardDuration by context.symphony.settings.seekForwardDuration.flow.collectAsState() - val controlsLayout by context.symphony.settings.nowPlayingControlsLayout.flow.collectAsState() - val lyricsLayout by context.symphony.settings.nowPlayingLyricsLayout.flow.collectAsState() + val isPlaying by context.symphony.radio.observatory.isPlaying.collectAsStateWithLifecycle() + val currentLoopMode by context.symphony.radio.observatory.loopMode.collectAsStateWithLifecycle() + val currentShuffleMode by context.symphony.radio.observatory.shuffleMode.collectAsStateWithLifecycle() + val currentSpeed by context.symphony.radio.observatory.speed.collectAsStateWithLifecycle() + val currentPitch by context.symphony.radio.observatory.pitch.collectAsStateWithLifecycle() + val persistedSpeed by context.symphony.radio.observatory.persistedSpeed.collectAsStateWithLifecycle() + val persistedPitch by context.symphony.radio.observatory.persistedPitch.collectAsStateWithLifecycle() + val sleepTimer by context.symphony.radio.observatory.sleepTimer.collectAsStateWithLifecycle() + val pauseOnCurrentSongEnd by context.symphony.radio.observatory.pauseOnCurrentSongEnd.collectAsStateWithLifecycle() + val showSongAdditionalInfo by context.symphony.settings.nowPlayingAdditionalInfo.flow.collectAsStateWithLifecycle() + val enableSeekControls by context.symphony.settings.nowPlayingSeekControls.flow.collectAsStateWithLifecycle() + val seekBackDuration by context.symphony.settings.seekBackDuration.flow.collectAsStateWithLifecycle() + val seekForwardDuration by context.symphony.settings.seekForwardDuration.flow.collectAsStateWithLifecycle() + val controlsLayout by context.symphony.settings.nowPlayingControlsLayout.flow.collectAsStateWithLifecycle() + val lyricsLayout by context.symphony.settings.nowPlayingLyricsLayout.flow.collectAsStateWithLifecycle() val data = when { isViable -> NowPlayingData( diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Playlist.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Playlist.kt index cef2d1a5..d40588a9 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Playlist.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Playlist.kt @@ -17,54 +17,49 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import io.github.zyrouge.symphony.services.groove.repositories.PlaylistRepository import io.github.zyrouge.symphony.ui.components.AnimatedNowPlayingBottomBar import io.github.zyrouge.symphony.ui.components.IconTextBody import io.github.zyrouge.symphony.ui.components.PlaylistDropdownMenu import io.github.zyrouge.symphony.ui.components.SongList -import io.github.zyrouge.symphony.ui.components.SongListType import io.github.zyrouge.symphony.ui.components.TopAppBarMinimalTitle import io.github.zyrouge.symphony.ui.helpers.ViewContext import io.github.zyrouge.symphony.ui.theme.ThemeColors -import io.github.zyrouge.symphony.utils.mutate +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.transformLatest import kotlinx.serialization.Serializable @Serializable data class PlaylistViewRoute(val playlistId: String) -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalCoroutinesApi::class) @Composable fun PlaylistView(context: ViewContext, route: PlaylistViewRoute) { - val allPlaylistIds by context.symphony.groove.playlist.all.collectAsState() - val updateId by context.symphony.groove.playlist.updateId.collectAsState() - var updateCounter by remember { mutableIntStateOf(0) } - val playlist by remember(route.playlistId, updateId) { - derivedStateOf { context.symphony.groove.playlist.get(route.playlistId) } - } - val songIds by remember(playlist) { - derivedStateOf { playlist?.getSongIds(context.symphony) ?: emptyList() } - } - val isViable by remember(allPlaylistIds, route.playlistId) { - derivedStateOf { allPlaylistIds.contains(route.playlistId) } - } - var showOptionsMenu by remember { mutableStateOf(false) } - val isFavoritesPlaylist by remember(playlist) { - derivedStateOf { - playlist?.let { context.symphony.groove.playlist.isFavoritesPlaylist(it) } == true + val playlistFlow = context.symphony.groove.playlist.findByIdAsFlow(route.playlistId) + val playlist by playlistFlow.collectAsStateWithLifecycle(null) + val songsSortBy by context.symphony.settings.lastUsedPlaylistSongsSortBy.flow.collectAsStateWithLifecycle() + val songsSortReverse by context.symphony.settings.lastUsedPlaylistSongsSortReverse.flow.collectAsStateWithLifecycle() + val songsFlow = playlistFlow.transformLatest { playlist -> + val value = when { + playlist == null -> emptyFlow() + else -> context.symphony.groove.playlist.findSongsByIdAsFlow( + playlist.entity.id, + songsSortBy, + songsSortReverse, + ) } + emitAll(value) } - - val incrementUpdateCounter = { - updateCounter = if (updateCounter > 25) 0 else updateCounter + 1 - } + val songs by songsFlow.collectAsStateWithLifecycle(emptyList()) + var showOptionsMenu by remember { mutableStateOf(false) } Scaffold( modifier = Modifier.fillMaxSize(), @@ -79,14 +74,11 @@ fun PlaylistView(context: ViewContext, route: PlaylistViewRoute) { }, title = { TopAppBarMinimalTitle { - Text( - context.symphony.t.Playlist - + (playlist?.let { " - ${it.title}" } ?: "") - ) + Text("${context.symphony.t.Playlist} - ${playlist?.entity?.title ?: context.symphony.t.UnknownSymbol}") } }, actions = { - if (isViable) { + if (playlist != null) { IconButton( onClick = { showOptionsMenu = true @@ -95,14 +87,9 @@ fun PlaylistView(context: ViewContext, route: PlaylistViewRoute) { Icon(Icons.Filled.MoreVert, null) PlaylistDropdownMenu( context, - playlist!!, + playlist = playlist!!, + songs = songs, expanded = showOptionsMenu, - onSongsChanged = { - incrementUpdateCounter() - }, - onRename = { - incrementUpdateCounter() - }, onDelete = { context.navController.popBackStack() }, @@ -125,33 +112,36 @@ fun PlaylistView(context: ViewContext, route: PlaylistViewRoute) { .fillMaxSize() ) { when { - isViable -> SongList( + playlist != null -> SongList( context, - songIds = songIds, - type = SongListType.Playlist, - disableHeartIcon = isFavoritesPlaylist, + songs = songs, + sortBy = songsSortBy, + sortReverse = songsSortReverse, + disableHeartIcon = playlist?.entity?.internalId == PlaylistRepository.PLAYLIST_INTERNAL_ID_FAVORITES, trailingOptionsContent = { _, song, onDismissRequest -> - playlist?.takeIf { it.isNotLocal }?.let { - DropdownMenuItem( - leadingIcon = { - Icon( - Icons.Filled.DeleteForever, - null, - tint = ThemeColors.Red, - ) - }, - text = { - Text(context.symphony.t.RemoveFromPlaylist) - }, - onClick = { - onDismissRequest() - context.symphony.groove.playlist.update( - it.id, - songIds.mutate { remove(song.id) }, - ) - } - ) - } + playlist + ?.takeIf { !it.entity.isModifiable } + ?.let { + DropdownMenuItem( + leadingIcon = { + Icon( + Icons.Filled.DeleteForever, + null, + tint = ThemeColors.Red, + ) + }, + text = { + Text(context.symphony.t.RemoveFromPlaylist) + }, + onClick = { + onDismissRequest() + context.symphony.groove.playlist.removeSongs( + it.entity.id, + listOf(song.id), + ) + } + ) + } }, ) diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Queue.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Queue.kt index 9c0a07d6..4f948eab 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Queue.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Queue.kt @@ -26,7 +26,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf @@ -53,8 +52,8 @@ object QueueViewRoute @Composable fun QueueView(context: ViewContext) { val coroutineScope = rememberCoroutineScope() - val queue by context.symphony.radio.observatory.queue.collectAsState() - val queueIndex by context.symphony.radio.observatory.queueIndex.collectAsState() + val queue by context.symphony.radio.observatory.queue.collectAsStateWithLifecycle() + val queueIndex by context.symphony.radio.observatory.queueIndex.collectAsStateWithLifecycle() val selectedSongIndices = remember { mutableStateListOf() } val listState = rememberLazyListState( initialFirstVisibleItemIndex = queueIndex, diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/AlbumArtists.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/AlbumArtists.kt index 7e53b503..6ec05645 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/AlbumArtists.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/AlbumArtists.kt @@ -1,7 +1,6 @@ package io.github.zyrouge.symphony.ui.view.home import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import io.github.zyrouge.symphony.ui.components.AlbumArtistGrid import io.github.zyrouge.symphony.ui.components.LoaderScaffold @@ -12,12 +11,13 @@ import kotlinx.coroutines.flow.mapLatest @OptIn(ExperimentalCoroutinesApi::class) @Composable fun AlbumArtistsView(context: ViewContext) { - val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsState() - val sortBy by context.symphony.settings.lastUsedAlbumArtistsSortBy.flow.collectAsState() - val sortReverse by context.symphony.settings.lastUsedAlbumArtistsSortReverse.flow.collectAsState() - val albumArtists by context.symphony.groove.albumArtist.valuesAsFlow(sortBy, sortReverse) - .mapLatest { it.map { x -> x.artist } } - .collectAsState(emptyList()) + val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsStateWithLifecycle() + val sortBy by context.symphony.settings.lastUsedAlbumArtistsSortBy.flow.collectAsStateWithLifecycle() + val sortReverse by context.symphony.settings.lastUsedAlbumArtistsSortReverse.flow.collectAsStateWithLifecycle() + val albumArtists by context.symphony.groove.artist + .valuesAsFlow(sortBy, sortReverse, onlyAlbumArtists = true) + .mapLatest { it.map { x -> x.entity } } + .collectAsStateWithLifecycle(emptyList()) LoaderScaffold(context, isLoading = isUpdating) { AlbumArtistGrid( diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Albums.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Albums.kt index 7dbb31fa..38a2f1db 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Albums.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Albums.kt @@ -1,7 +1,6 @@ package io.github.zyrouge.symphony.ui.view.home import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import io.github.zyrouge.symphony.ui.components.AlbumGrid import io.github.zyrouge.symphony.ui.components.LoaderScaffold @@ -12,12 +11,12 @@ import kotlinx.coroutines.flow.mapLatest @OptIn(ExperimentalCoroutinesApi::class) @Composable fun AlbumsView(context: ViewContext) { - val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsState() - val sortBy by context.symphony.settings.lastUsedAlbumsSortBy.flow.collectAsState() - val sortReverse by context.symphony.settings.lastUsedAlbumsSortReverse.flow.collectAsState() + val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsStateWithLifecycle() + val sortBy by context.symphony.settings.lastUsedAlbumsSortBy.flow.collectAsStateWithLifecycle() + val sortReverse by context.symphony.settings.lastUsedAlbumsSortReverse.flow.collectAsStateWithLifecycle() val albums by context.symphony.groove.album.valuesAsFlow(sortBy, sortReverse) - .mapLatest { it.map { x -> x.album } } - .collectAsState(emptyList()) + .mapLatest { it.map { x -> x.entity } } + .collectAsStateWithLifecycle(emptyList()) LoaderScaffold(context, isLoading = isUpdating) { AlbumGrid( diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Artists.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Artists.kt index 4b80e229..d710a57c 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Artists.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Artists.kt @@ -1,7 +1,6 @@ package io.github.zyrouge.symphony.ui.view.home import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import io.github.zyrouge.symphony.ui.components.ArtistGrid import io.github.zyrouge.symphony.ui.components.LoaderScaffold @@ -12,12 +11,12 @@ import kotlinx.coroutines.flow.mapLatest @OptIn(ExperimentalCoroutinesApi::class) @Composable fun ArtistsView(context: ViewContext) { - val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsState() - val sortBy by context.symphony.settings.lastUsedArtistsSortBy.flow.collectAsState() - val sortReverse by context.symphony.settings.lastUsedArtistsSortReverse.flow.collectAsState() + val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsStateWithLifecycle() + val sortBy by context.symphony.settings.lastUsedArtistsSortBy.flow.collectAsStateWithLifecycle() + val sortReverse by context.symphony.settings.lastUsedArtistsSortReverse.flow.collectAsStateWithLifecycle() val artists by context.symphony.groove.artist.valuesAsFlow(sortBy, sortReverse) - .mapLatest { it.map { x -> x.artist } } - .collectAsState(emptyList()) + .mapLatest { it.map { x -> x.entity } } + .collectAsStateWithLifecycle(emptyList()) LoaderScaffold(context, isLoading = isUpdating) { ArtistGrid( diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Browser.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Browser.kt index f1ffe09f..217987a0 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Browser.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Browser.kt @@ -1,28 +1,24 @@ package io.github.zyrouge.symphony.ui.view.home import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import io.github.zyrouge.symphony.ui.components.LoaderScaffold -import io.github.zyrouge.symphony.ui.components.SongExplorerList import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.utils.SimplePath @Composable fun BrowserView(context: ViewContext) { - val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsState() - val explorer = context.symphony.groove.song.explorer - val lastUsedFolderPath by context.symphony.settings.lastUsedBrowserPath.flow.collectAsState() + val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsStateWithLifecycle() + val lastUsedFolderPath by context.symphony.settings.lastUsedBrowserPath.flow.collectAsStateWithLifecycle() LoaderScaffold(context, isLoading = isUpdating) { - SongExplorerList( - context, - initialPath = lastUsedFolderPath?.let { SimplePath(it) }, - key = id, - explorer = explorer, - onPathChange = { path -> - context.symphony.settings.lastUsedBrowserPath.setValue(path.pathString) - } - ) +// SongExplorerList( +// context, +// initialPath = lastUsedFolderPath?.let { SimplePath(it) }, +// key = id, +// explorer = explorer, +// onPathChange = { path -> +// context.symphony.settings.lastUsedBrowserPath.setValue(path.pathString) +// } +// ) } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Folders.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Folders.kt index 7d69f37a..dfcc92b7 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Folders.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Folders.kt @@ -1,350 +1,75 @@ package io.github.zyrouge.symphony.ui.view.home -import androidx.activity.compose.BackHandler -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.togetherWith -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.grid.itemsIndexed -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.PlaylistAdd -import androidx.compose.material.icons.automirrored.filled.PlaylistPlay -import androidx.compose.material.icons.filled.FolderCopy -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import io.github.zyrouge.symphony.services.groove.Groove -import io.github.zyrouge.symphony.ui.components.AddToPlaylistDialog -import io.github.zyrouge.symphony.ui.components.IconTextBody +import io.github.zyrouge.symphony.services.database.store.valuesAsFlow import io.github.zyrouge.symphony.ui.components.LoaderScaffold -import io.github.zyrouge.symphony.ui.components.MediaSortBar -import io.github.zyrouge.symphony.ui.components.MediaSortBarScaffold -import io.github.zyrouge.symphony.ui.components.ResponsiveGrid -import io.github.zyrouge.symphony.ui.components.ResponsiveGridColumns -import io.github.zyrouge.symphony.ui.components.ResponsiveGridSizeAdjustBottomSheet -import io.github.zyrouge.symphony.ui.components.SongList -import io.github.zyrouge.symphony.ui.components.SquareGrooveTile -import io.github.zyrouge.symphony.ui.components.label -import io.github.zyrouge.symphony.ui.helpers.Assets -import io.github.zyrouge.symphony.ui.helpers.FadeTransition -import io.github.zyrouge.symphony.ui.helpers.SlideTransition import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.ui.view.nowPlaying.defaultHorizontalPadding -import io.github.zyrouge.symphony.utils.SimpleFileSystem -import io.github.zyrouge.symphony.utils.StringListUtils -import java.util.Stack @Composable fun FoldersView(context: ViewContext) { - val isUpdating by context.symphony.groove.song.isUpdating.collectAsState() - val id by context.symphony.groove.song.id.collectAsState() - val explorer = context.symphony.groove.song.explorer - - val folders = remember(id) { - val entities = mutableMapOf() - val stack = Stack() - stack.add(explorer) - while (stack.isNotEmpty()) { - val current = stack.pop() - if (current.isEmpty) continue - var hasSongs = false - current.children.values.forEach { - when (it) { - is SimpleFileSystem.Folder -> stack.push(it) - is SimpleFileSystem.File -> { - hasSongs = true - } - } - } - if (hasSongs) { - entities[current.fullPath.pathString] = current - } - } - entities.toMap() - } - var currentFolder by remember(id) { - mutableStateOf(null) - } - - BackHandler(currentFolder != null) { - currentFolder = null - } + val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsStateWithLifecycle() + val sortBy by context.symphony.settings.lastUsedFoldersSortBy.flow.collectAsStateWithLifecycle() + val sortReverse by context.symphony.settings.lastUsedFoldersSortReverse.flow.collectAsStateWithLifecycle() + val folders by context.symphony.database.mediaTreeFolders.valuesAsFlow(sortBy, sortReverse) + .collectAsStateWithLifecycle(emptyList()) LoaderScaffold(context, isLoading = isUpdating) { - AnimatedContent( - label = "folders-view-content", - targetState = currentFolder, - transitionSpec = { - val enter = when { - targetState != null -> SlideTransition.slideUp.enterTransition() - else -> FadeTransition.enterTransition() - } - enter.togetherWith(FadeTransition.exitTransition()) - }, - ) { folder -> - if (folder != null) { - val songIds by remember(folder) { - derivedStateOf { - folder.children.values.mapNotNull { - when (it) { - is SimpleFileSystem.File -> it.data as String - else -> null - } - } - } - } - - Column { - Column( - modifier = Modifier.padding( - start = defaultHorizontalPadding, - end = defaultHorizontalPadding, - top = 4.dp, - bottom = 12.dp, - ), - ) { - folder.parent?.let { parent -> - Text( - "${parent.fullPath}/", - style = MaterialTheme.typography.bodyMedium.copy( - color = LocalContentColor.current.copy(alpha = 0.7f), - ), - ) - } - Text(folder.name, style = MaterialTheme.typography.bodyLarge) - } - HorizontalDivider() - SongList(context, songIds = songIds, songsCount = songIds.size) - } - } else { - FoldersGrid( - context, - folders = folders, - onClick = { - currentFolder = it - } - ) - } - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun FoldersGrid( - context: ViewContext, - folders: Map, - onClick: (SimpleFileSystem.Folder) -> Unit, -) { - val sortBy by context.symphony.settings.lastUsedFoldersSortBy.flow.collectAsState() - val sortReverse by context.symphony.settings.lastUsedFoldersSortReverse.flow.collectAsState() - val sortedFolderNames by remember(folders, sortBy, sortReverse) { - derivedStateOf { - StringListUtils.sort(folders.keys.toList(), sortBy, sortReverse) - } - } - val horizontalGridColumns by context.symphony.settings.lastUsedFoldersHorizontalGridColumns.flow.collectAsState() - val verticalGridColumns by context.symphony.settings.lastUsedFoldersVerticalGridColumns.flow.collectAsState() - val gridColumns by remember(horizontalGridColumns, verticalGridColumns) { - derivedStateOf { - ResponsiveGridColumns(horizontalGridColumns, verticalGridColumns) - } - } - var showModifyLayoutSheet by remember { mutableStateOf(false) } - - MediaSortBarScaffold( - mediaSortBar = { - MediaSortBar( - context, - reverse = sortReverse, - onReverseChange = { - context.symphony.settings.lastUsedFoldersSortReverse.setValue(it) - }, - sort = sortBy, - sorts = StringListUtils.SortBy.entries - .associateWith { x -> ViewContext.parameterizedFn { x.label(context) } }, - onSortChange = { - context.symphony.settings.lastUsedFoldersSortBy.setValue(it) - }, - label = { - Text(context.symphony.t.XFolders(folders.size.toString())) - }, - onShowModifyLayout = { - showModifyLayoutSheet = true - } - ) - }, - content = { - when { - sortedFolderNames.isEmpty() -> IconTextBody( - icon = { modifier -> - Icon( - Icons.Filled.FolderCopy, - null, - modifier = modifier, - ) - }, - content = { Text(context.symphony.t.DamnThisIsSoEmpty) } - ) - - else -> ResponsiveGrid(gridColumns) { - itemsIndexed( - sortedFolderNames, - key = { i, x -> "$i-$x" }, - contentType = { _, _ -> Groove.Kind.ARTIST } - ) { _, folderName -> - folders[folderName]?.let { folder -> - FolderTile( - context, folder = folder, - onClick = { onClick(folder) }, - ) - } - } - } - } - - if (showModifyLayoutSheet) { - ResponsiveGridSizeAdjustBottomSheet( - context, - columns = gridColumns, - onColumnsChange = { - context.symphony.settings.lastUsedFoldersHorizontalGridColumns.setValue( - it.horizontal - ) - context.symphony.settings.lastUsedFoldersVerticalGridColumns.setValue( - it.vertical - ) - }, - onDismissRequest = { - showModifyLayoutSheet = false - } - ) - } - } - ) -} - -@Composable -private fun FolderTile( - context: ViewContext, - folder: SimpleFileSystem.Folder, - onClick: () -> Unit, -) { - SquareGrooveTile( - image = folder.createArtworkImageRequest(context).build(), - options = { expanded, onDismissRequest -> - var showAddToPlaylistDialog by remember { mutableStateOf(false) } - - DropdownMenu( - expanded = expanded, - onDismissRequest = onDismissRequest - ) { - DropdownMenuItem( - leadingIcon = { - Icon(Icons.AutoMirrored.Filled.PlaylistPlay, null) - }, - text = { - Text(context.symphony.t.ShufflePlay) - }, - onClick = { - onDismissRequest() - context.symphony.radio.shorty.playQueue( - folder.getSortedSongIds(context), - shuffle = true, - ) - } - ) - DropdownMenuItem( - leadingIcon = { - Icon(Icons.AutoMirrored.Filled.PlaylistPlay, null) - }, - text = { - Text(context.symphony.t.PlayNext) - }, - onClick = { - onDismissRequest() - context.symphony.radio.queue.add( - folder.getSortedSongIds(context), - context.symphony.radio.queue.currentSongIndex + 1 - ) - } - ) - DropdownMenuItem( - leadingIcon = { - Icon(Icons.AutoMirrored.Filled.PlaylistAdd, null) - }, - text = { - Text(context.symphony.t.AddToPlaylist) - }, - onClick = { - onDismissRequest() - showAddToPlaylistDialog = true - } - ) - } - - if (showAddToPlaylistDialog) { - AddToPlaylistDialog( - context, - songIds = folder.getSortedSongIds(context), - onDismissRequest = { - showAddToPlaylistDialog = false - } - ) - } - }, - content = { - Text( - folder.name, - style = MaterialTheme.typography.bodyMedium, - textAlign = TextAlign.Center, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) - }, - onPlay = { - val sortedSongIds = folder.getSortedSongIds(context) - context.symphony.radio.shorty.playQueue(sortedSongIds) - }, - onClick = onClick, - ) -} - -private fun SimpleFileSystem.Folder.createArtworkImageRequest(context: ViewContext) = - children.values - .find { it is SimpleFileSystem.File } - ?.let { - val songId = (it as SimpleFileSystem.File).data as String - context.symphony.groove.song.createArtworkImageRequest(songId) - } - ?: Assets.createPlaceholderImageRequest(context.symphony) - -private fun SimpleFileSystem.Folder.getSortedSongIds(context: ViewContext): List { - val songIds = children.values.mapNotNull { - when (it) { - is SimpleFileSystem.File -> it.data as String - else -> null - } +// AnimatedContent( +// label = "folders-view-content", +// targetState = currentFolder, +// transitionSpec = { +// val enter = when { +// targetState != null -> SlideTransition.slideUp.enterTransition() +// else -> FadeTransition.enterTransition() +// } +// enter.togetherWith(FadeTransition.exitTransition()) +// }, +// ) { folder -> +// if (folder != null) { +// val songIds by remember(folder) { +// derivedStateOf { +// folder.children.values.mapNotNull { +// when (it) { +// is SimpleFileSystem.File -> it.data as String +// else -> null +// } +// } +// } +// } +// +// Column { +// Column( +// modifier = Modifier.padding( +// start = defaultHorizontalPadding, +// end = defaultHorizontalPadding, +// top = 4.dp, +// bottom = 12.dp, +// ), +// ) { +// folder.parent?.let { parent -> +// Text( +// "${parent.fullPath}/", +// style = MaterialTheme.typography.bodyMedium.copy( +// color = LocalContentColor.current.copy(alpha = 0.7f), +// ), +// ) +// } +// Text(folder.name, style = MaterialTheme.typography.bodyLarge) +// } +// HorizontalDivider() +// SongList(context, songIds = songIds, songsCount = songIds.size) +// } +// } else { +// FoldersGrid( +// context, +// folders = folders, +// onClick = { +// currentFolder = it +// } +// ) +// } +// } +// } } - return context.symphony.groove.song.sort( - songIds, - context.symphony.settings.lastUsedSongsSortBy.value, - context.symphony.settings.lastUsedSongsSortReverse.value, - ) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/ForYou.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/ForYou.kt index a146a0b3..7e27fe84 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/ForYou.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/ForYou.kt @@ -34,7 +34,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -68,16 +67,16 @@ enum class ForYou(val label: (context: ViewContext) -> String) { @OptIn(ExperimentalMaterial3Api::class) @Composable fun ForYouView(context: ViewContext) { - val albumArtistsIsUpdating by context.symphony.groove.albumArtist.isUpdating.collectAsState() - val albumsIsUpdating by context.symphony.groove.album.isUpdating.collectAsState() - val artistsIsUpdating by context.symphony.groove.artist.isUpdating.collectAsState() - val songsIsUpdating by context.symphony.groove.song.isUpdating.collectAsState() - val albumArtistNames by context.symphony.groove.albumArtist.all.collectAsState() - val albumIds by context.symphony.groove.album.all.collectAsState() - val artistNames by context.symphony.groove.artist.all.collectAsState() - val songIds by context.symphony.groove.song.all.collectAsState() - val sortBy by context.symphony.settings.lastUsedSongsSortBy.flow.collectAsState() - val sortReverse by context.symphony.settings.lastUsedSongsSortReverse.flow.collectAsState() + val albumArtistsIsUpdating by context.symphony.groove.albumArtist.isUpdating.collectAsStateWithLifecycle() + val albumsIsUpdating by context.symphony.groove.album.isUpdating.collectAsStateWithLifecycle() + val artistsIsUpdating by context.symphony.groove.artist.isUpdating.collectAsStateWithLifecycle() + val songsIsUpdating by context.symphony.groove.song.isUpdating.collectAsStateWithLifecycle() + val albumArtistNames by context.symphony.groove.albumArtist.all.collectAsStateWithLifecycle() + val albumIds by context.symphony.groove.album.all.collectAsStateWithLifecycle() + val artistNames by context.symphony.groove.artist.all.collectAsStateWithLifecycle() + val songIds by context.symphony.groove.song.all.collectAsStateWithLifecycle() + val sortBy by context.symphony.settings.lastUsedSongsSortBy.flow.collectAsStateWithLifecycle() + val sortReverse by context.symphony.settings.lastUsedSongsSortReverse.flow.collectAsStateWithLifecycle() when { songIds.isNotEmpty() -> { @@ -265,7 +264,7 @@ fun ForYouView(context: ViewContext) { } } } - val contents by context.symphony.settings.forYouContents.flow.collectAsState() + val contents by context.symphony.settings.forYouContents.flow.collectAsStateWithLifecycle() contents.forEach { when (it) { ForYou.Albums -> SuggestedAlbums( diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Genres.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Genres.kt index 4ae49cb8..a7dbde7f 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Genres.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Genres.kt @@ -1,19 +1,19 @@ package io.github.zyrouge.symphony.ui.view.home import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.zyrouge.symphony.ui.components.GenreGrid import io.github.zyrouge.symphony.ui.components.LoaderScaffold import io.github.zyrouge.symphony.ui.helpers.ViewContext @Composable fun GenresView(context: ViewContext) { - val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsState() - val sortBy by context.symphony.settings.lastUsedGenresSortBy.flow.collectAsState() - val sortReverse by context.symphony.settings.lastUsedGenresSortReverse.flow.collectAsState() + val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsStateWithLifecycle() + val sortBy by context.symphony.settings.lastUsedGenresSortBy.flow.collectAsStateWithLifecycle() + val sortReverse by context.symphony.settings.lastUsedGenresSortReverse.flow.collectAsStateWithLifecycle() val attributedGenres by context.symphony.groove.genre.valuesAsFlow(sortBy, sortReverse) - .collectAsState(emptyList()) + .collectAsStateWithLifecycle(emptyList()) LoaderScaffold(context, isLoading = isUpdating) { GenreGrid( diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Playlists.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Playlists.kt index 5090471e..160eaac6 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Playlists.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Playlists.kt @@ -17,7 +17,6 @@ import androidx.compose.material3.ElevatedButton import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -25,8 +24,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import io.github.zyrouge.symphony.services.groove.Playlist import io.github.zyrouge.symphony.services.groove.entities.Playlist +import io.github.zyrouge.symphony.services.groove.repositories.PlaylistRepository import io.github.zyrouge.symphony.ui.components.LoaderScaffold import io.github.zyrouge.symphony.ui.components.NewPlaylistDialog import io.github.zyrouge.symphony.ui.components.PlaylistGrid @@ -39,12 +38,12 @@ import kotlinx.coroutines.flow.mapLatest @OptIn(ExperimentalCoroutinesApi::class) @Composable fun PlaylistsView(context: ViewContext) { - val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsState() - val sortBy by context.symphony.settings.lastUsedPlaylistsSortBy.flow.collectAsState() - val sortReverse by context.symphony.settings.lastUsedPlaylistsSortReverse.flow.collectAsState() + val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsStateWithLifecycle() + val sortBy by context.symphony.settings.lastUsedPlaylistsSortBy.flow.collectAsStateWithLifecycle() + val sortReverse by context.symphony.settings.lastUsedPlaylistsSortReverse.flow.collectAsStateWithLifecycle() val playlists by context.symphony.groove.playlist.valuesAsFlow(sortBy, sortReverse) - .mapLatest { it.map { x -> x.playlist } } - .collectAsState(emptyList()) + .mapLatest { it.map { x -> x.entity } } + .collectAsStateWithLifecycle(emptyList()) var showPlaylistCreator by remember { mutableStateOf(false) } val openPlaylistLauncher = rememberLauncherForActivityResult( @@ -53,8 +52,13 @@ fun PlaylistsView(context: ViewContext) { uris.forEach { x -> try { ActivityUtils.makePersistableReadableUri(context.symphony.applicationContext, x) - val playlist = Playlist.parse(context.symphony, null, x) - context.symphony.groove.playlist.add(playlist) + val id = context.symphony.database.playlistsIdGenerator.next() + val parsed = Playlist.parse(context.symphony, id, x) + val addOptions = PlaylistRepository.AddOptions( + playlist = parsed.playlist, + songPaths = parsed.songPaths, + ) + context.symphony.groove.playlist.add(addOptions) } catch (err: Exception) { Logger.error("PlaylistView", "import failed (activity result)", err) Toast.makeText( @@ -90,9 +94,9 @@ fun PlaylistsView(context: ViewContext) { if (showPlaylistCreator) { NewPlaylistDialog( context, - onDone = { playlist -> + onDone = { addOptions -> showPlaylistCreator = false - context.symphony.groove.playlist.add(playlist) + context.symphony.groove.playlist.add(addOptions) }, onDismissRequest = { showPlaylistCreator = false diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Songs.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Songs.kt index 282b3fcd..4d1b09ce 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Songs.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Songs.kt @@ -1,23 +1,20 @@ package io.github.zyrouge.symphony.ui.view.home import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import io.github.zyrouge.symphony.ui.components.LoaderScaffold import io.github.zyrouge.symphony.ui.components.SongList import io.github.zyrouge.symphony.ui.helpers.ViewContext import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.transformLatest @OptIn(ExperimentalCoroutinesApi::class) @Composable fun SongsView(context: ViewContext) { - val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsState() - val sortBy by context.symphony.settings.lastUsedSongsSortBy.flow.collectAsState() - val sortReverse by context.symphony.settings.lastUsedSongsSortReverse.flow.collectAsState() + val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsStateWithLifecycle() + val sortBy by context.symphony.settings.lastUsedSongsSortBy.flow.collectAsStateWithLifecycle() + val sortReverse by context.symphony.settings.lastUsedSongsSortReverse.flow.collectAsStateWithLifecycle() val songs by context.symphony.groove.song.valuesAsFlow(sortBy, sortReverse) - .transformLatest { emit(it.map { x -> x.song }) } - .collectAsState(emptyList()) + .collectAsStateWithLifecycle(emptyList()) LoaderScaffold(context, isLoading = isUpdating) { SongList( diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Tree.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Tree.kt index 88f2a0e3..1f36254b 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Tree.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Tree.kt @@ -1,28 +1,24 @@ package io.github.zyrouge.symphony.ui.view.home import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import io.github.zyrouge.symphony.ui.components.LoaderScaffold -import io.github.zyrouge.symphony.ui.components.SongTreeList +import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.zyrouge.symphony.ui.helpers.ViewContext @Composable fun TreeView(context: ViewContext) { - val isUpdating by context.symphony.groove.song.isUpdating.collectAsState() - val songIds by context.symphony.groove.song.all.collectAsState() - val songsCount by context.symphony.groove.song.count.collectAsState() - val disabledTreePaths by context.symphony.settings.lastDisabledTreePaths.flow.collectAsState() + val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsStateWithLifecycle() +// val disabledTreePaths by context.symphony.settings.lastDisabledTreePaths.flow.collectAsStateWithLifecycle() - LoaderScaffold(context, isLoading = isUpdating) { - SongTreeList( - context, - songIds = songIds, - songsCount = songsCount, - initialDisabled = disabledTreePaths.toList(), - onDisable = { paths -> - context.symphony.settings.lastDisabledTreePaths.setValue(paths.toSet()) - }, - ) - } +// LoaderScaffold(context, isLoading = isUpdating) { +// SongTreeList( +// context, +// songIds = songIds, +// songsCount = songsCount, +// initialDisabled = disabledTreePaths.toList(), +// onDisable = { paths -> +// context.symphony.settings.lastDisabledTreePaths.setValue(paths.toSet()) +// }, +// ) +// } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BodyContent.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BodyContent.kt index 68e36687..f7661ad2 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BodyContent.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BodyContent.kt @@ -36,7 +36,6 @@ import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf @@ -62,7 +61,7 @@ import io.github.zyrouge.symphony.utils.DurationUtils @OptIn(ExperimentalLayoutApi::class) @Composable fun NowPlayingBodyContent(context: ViewContext, data: NowPlayingData) { - val favoriteSongIds by context.symphony.groove.playlist.favorites.collectAsState() + val favoriteSongIds by context.symphony.groove.playlist.favorites.collectAsStateWithLifecycle() val isFavorite by remember(data) { derivedStateOf { favoriteSongIds.contains(data.song.id) } } @@ -289,7 +288,7 @@ fun NowPlayingTraditionalControls(context: ViewContext, data: NowPlayingData) { @Composable fun NowPlayingSeekBar(context: ViewContext) { - val playbackPosition by context.symphony.radio.observatory.playbackPosition.collectAsState() + val playbackPosition by context.symphony.radio.observatory.playbackPosition.collectAsStateWithLifecycle() Row( modifier = Modifier.padding(defaultHorizontalPadding, 0.dp), diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BodyCover.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BodyCover.kt index d6ae3eaf..1deb0a66 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BodyCover.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BodyCover.kt @@ -15,7 +15,6 @@ import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -45,7 +44,7 @@ fun NowPlayingBodyCover( states: NowPlayingStates, orientation: ScreenOrientation, ) { - val showLyrics by states.showLyrics.collectAsState() + val showLyrics by states.showLyrics.collectAsStateWithLifecycle() Box(modifier = Modifier.padding(defaultHorizontalPadding, 0.dp)) { AnimatedContent( @@ -69,12 +68,12 @@ fun NowPlayingBodyCover( @Composable private fun NowPlayingBodyCoverLyrics(context: ViewContext, orientation: ScreenOrientation) { - val keepScreenAwake by context.symphony.settings.lyricsKeepScreenAwake.flow.collectAsState() + val keepScreenAwake by context.symphony.settings.lyricsKeepScreenAwake.flow.collectAsStateWithLifecycle() if (keepScreenAwake) { KeepScreenAwake() } - + Box( modifier = Modifier .fillMaxSize() diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BottomBar.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BottomBar.kt index ad354ec1..047e008d 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BottomBar.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BottomBar.kt @@ -36,7 +36,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -69,7 +68,7 @@ fun NowPlayingBodyBottomBar( context.symphony.radio.session.createEqualizerActivityContract() ) {} - val sleepTimer by context.symphony.radio.observatory.sleepTimer.collectAsState() + val sleepTimer by context.symphony.radio.observatory.sleepTimer.collectAsStateWithLifecycle() var showSleepTimerDialog by remember { mutableStateOf(false) } var showSpeedDialog by remember { mutableStateOf(false) } var showPitchDialog by remember { mutableStateOf(false) } @@ -107,7 +106,7 @@ fun NowPlayingBodyBottomBar( } Spacer(modifier = Modifier.weight(1f)) states.showLyrics.let { showLyricsState -> - val showLyrics by showLyricsState.collectAsState() + val showLyrics by showLyricsState.collectAsStateWithLifecycle() IconButton( onClick = { diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/AppearanceSettingsView.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/AppearanceSettingsView.kt index 682bc076..19bb31f5 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/AppearanceSettingsView.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/AppearanceSettingsView.kt @@ -24,7 +24,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -56,13 +55,13 @@ object AppearanceSettingsViewRoute @Composable fun AppearanceSettingsView(context: ViewContext) { val scrollState = rememberScrollState() - val language by context.symphony.settings.language.flow.collectAsState() - val fontFamily by context.symphony.settings.fontFamily.flow.collectAsState() - val themeMode by context.symphony.settings.themeMode.flow.collectAsState() - val useMaterialYou by context.symphony.settings.useMaterialYou.flow.collectAsState() - val primaryColor by context.symphony.settings.primaryColor.flow.collectAsState() - val fontScale by context.symphony.settings.fontScale.flow.collectAsState() - val contentScale by context.symphony.settings.contentScale.flow.collectAsState() + val language by context.symphony.settings.language.flow.collectAsStateWithLifecycle() + val fontFamily by context.symphony.settings.fontFamily.flow.collectAsStateWithLifecycle() + val themeMode by context.symphony.settings.themeMode.flow.collectAsStateWithLifecycle() + val useMaterialYou by context.symphony.settings.useMaterialYou.flow.collectAsStateWithLifecycle() + val primaryColor by context.symphony.settings.primaryColor.flow.collectAsStateWithLifecycle() + val fontScale by context.symphony.settings.fontScale.flow.collectAsStateWithLifecycle() + val contentScale by context.symphony.settings.contentScale.flow.collectAsStateWithLifecycle() Scaffold( modifier = Modifier.fillMaxSize(), diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/GrooveSettingsView.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/GrooveSettingsView.kt index 6e134311..6015b4d0 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/GrooveSettingsView.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/GrooveSettingsView.kt @@ -35,7 +35,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -75,16 +74,16 @@ fun GrooveSettingsView(context: ViewContext, route: GrooveSettingsViewRoute) { val coroutineScope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } val scrollState = rememberScrollState() - val songsFilterPattern by context.symphony.settings.songsFilterPattern.flow.collectAsState() - val minSongDuration by context.symphony.settings.minSongDuration.flow.collectAsState() - val blacklistFolders by context.symphony.settings.blacklistFolders.flow.collectAsState() - val whitelistFolders by context.symphony.settings.whitelistFolders.flow.collectAsState() - val artistTagSeparators by context.symphony.settings.artistTagSeparators.flow.collectAsState() - val genreTagSeparators by context.symphony.settings.genreTagSeparators.flow.collectAsState() - val mediaFolders by context.symphony.settings.mediaFolders.flow.collectAsState() - val artworkQuality by context.symphony.settings.artworkQuality.flow.collectAsState() - val caseSensitiveSorting by context.symphony.settings.caseSensitiveSorting.flow.collectAsState() - val useMetaphony by context.symphony.settings.useMetaphony.flow.collectAsState() + val songsFilterPattern by context.symphony.settings.songsFilterPattern.flow.collectAsStateWithLifecycle() + val minSongDuration by context.symphony.settings.minSongDuration.flow.collectAsStateWithLifecycle() + val blacklistFolders by context.symphony.settings.blacklistFolders.flow.collectAsStateWithLifecycle() + val whitelistFolders by context.symphony.settings.whitelistFolders.flow.collectAsStateWithLifecycle() + val artistTagSeparators by context.symphony.settings.artistTagSeparators.flow.collectAsStateWithLifecycle() + val genreTagSeparators by context.symphony.settings.genreTagSeparators.flow.collectAsStateWithLifecycle() + val mediaFolders by context.symphony.settings.mediaFolders.flow.collectAsStateWithLifecycle() + val artworkQuality by context.symphony.settings.artworkQuality.flow.collectAsStateWithLifecycle() + val caseSensitiveSorting by context.symphony.settings.caseSensitiveSorting.flow.collectAsStateWithLifecycle() + val useMetaphony by context.symphony.settings.useMetaphony.flow.collectAsStateWithLifecycle() Scaffold( modifier = Modifier.fillMaxSize(), diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/HomePageSettingsView.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/HomePageSettingsView.kt index a98e76c0..0690f7cb 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/HomePageSettingsView.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/HomePageSettingsView.kt @@ -20,7 +20,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -43,9 +42,9 @@ object HomePageSettingsViewRoute @Composable fun HomePageSettingsView(context: ViewContext) { val scrollState = rememberScrollState() - val homeTabs by context.symphony.settings.homeTabs.flow.collectAsState() - val forYouContents by context.symphony.settings.forYouContents.flow.collectAsState() - val homePageBottomBarLabelVisibility by context.symphony.settings.homePageBottomBarLabelVisibility.flow.collectAsState() + val homeTabs by context.symphony.settings.homeTabs.flow.collectAsStateWithLifecycle() + val forYouContents by context.symphony.settings.forYouContents.flow.collectAsStateWithLifecycle() + val homePageBottomBarLabelVisibility by context.symphony.settings.homePageBottomBarLabelVisibility.flow.collectAsStateWithLifecycle() Scaffold( modifier = Modifier.fillMaxSize(), diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/MiniPlayerSettingsView.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/MiniPlayerSettingsView.kt index fbcffd8f..3c4c6abd 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/MiniPlayerSettingsView.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/MiniPlayerSettingsView.kt @@ -20,7 +20,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -39,9 +38,9 @@ object MiniPlayerSettingsViewRoute @Composable fun MiniPlayerSettingsView(context: ViewContext) { val scrollState = rememberScrollState() - val miniPlayerTrackControls by context.symphony.settings.miniPlayerTrackControls.flow.collectAsState() - val miniPlayerSeekControls by context.symphony.settings.miniPlayerSeekControls.flow.collectAsState() - val miniPlayerTextMarquee by context.symphony.settings.miniPlayerTextMarquee.flow.collectAsState() + val miniPlayerTrackControls by context.symphony.settings.miniPlayerTrackControls.flow.collectAsStateWithLifecycle() + val miniPlayerSeekControls by context.symphony.settings.miniPlayerSeekControls.flow.collectAsStateWithLifecycle() + val miniPlayerTextMarquee by context.symphony.settings.miniPlayerTextMarquee.flow.collectAsStateWithLifecycle() Scaffold( modifier = Modifier.fillMaxSize(), diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/NowPlayingSettingsView.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/NowPlayingSettingsView.kt index b62f6c27..5f37d613 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/NowPlayingSettingsView.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/NowPlayingSettingsView.kt @@ -22,7 +22,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -44,11 +43,11 @@ object NowPlayingSettingsViewRoute @Composable fun NowPlayingSettingsView(context: ViewContext) { val scrollState = rememberScrollState() - val nowPlayingControlsLayout by context.symphony.settings.nowPlayingControlsLayout.flow.collectAsState() - val nowPlayingAdditionalInfo by context.symphony.settings.nowPlayingAdditionalInfo.flow.collectAsState() - val nowPlayingSeekControls by context.symphony.settings.nowPlayingSeekControls.flow.collectAsState() - val nowPlayingLyricsLayout by context.symphony.settings.nowPlayingLyricsLayout.flow.collectAsState() - val lyricsKeepScreenAwake by context.symphony.settings.lyricsKeepScreenAwake.flow.collectAsState() + val nowPlayingControlsLayout by context.symphony.settings.nowPlayingControlsLayout.flow.collectAsStateWithLifecycle() + val nowPlayingAdditionalInfo by context.symphony.settings.nowPlayingAdditionalInfo.flow.collectAsStateWithLifecycle() + val nowPlayingSeekControls by context.symphony.settings.nowPlayingSeekControls.flow.collectAsStateWithLifecycle() + val nowPlayingLyricsLayout by context.symphony.settings.nowPlayingLyricsLayout.flow.collectAsStateWithLifecycle() + val lyricsKeepScreenAwake by context.symphony.settings.lyricsKeepScreenAwake.flow.collectAsStateWithLifecycle() Scaffold( modifier = Modifier.fillMaxSize(), diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/PlayerSettingsView.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/PlayerSettingsView.kt index 8438d7fb..dda33249 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/PlayerSettingsView.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/PlayerSettingsView.kt @@ -24,7 +24,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -45,15 +44,15 @@ object PlayerSettingsViewRoute @Composable fun PlayerSettingsView(context: ViewContext) { val scrollState = rememberScrollState() - val fadePlayback by context.symphony.settings.fadePlayback.flow.collectAsState() - val fadePlaybackDuration by context.symphony.settings.fadePlaybackDuration.flow.collectAsState() - val requireAudioFocus by context.symphony.settings.requireAudioFocus.flow.collectAsState() - val ignoreAudioFocusLoss by context.symphony.settings.ignoreAudioFocusLoss.flow.collectAsState() - val playOnHeadphonesConnect by context.symphony.settings.playOnHeadphonesConnect.flow.collectAsState() - val pauseOnHeadphonesDisconnect by context.symphony.settings.pauseOnHeadphonesDisconnect.flow.collectAsState() - val seekBackDuration by context.symphony.settings.seekBackDuration.flow.collectAsState() - val seekForwardDuration by context.symphony.settings.seekForwardDuration.flow.collectAsState() - val gaplessPlayback by context.symphony.settings.gaplessPlayback.flow.collectAsState() + val fadePlayback by context.symphony.settings.fadePlayback.flow.collectAsStateWithLifecycle() + val fadePlaybackDuration by context.symphony.settings.fadePlaybackDuration.flow.collectAsStateWithLifecycle() + val requireAudioFocus by context.symphony.settings.requireAudioFocus.flow.collectAsStateWithLifecycle() + val ignoreAudioFocusLoss by context.symphony.settings.ignoreAudioFocusLoss.flow.collectAsStateWithLifecycle() + val playOnHeadphonesConnect by context.symphony.settings.playOnHeadphonesConnect.flow.collectAsStateWithLifecycle() + val pauseOnHeadphonesDisconnect by context.symphony.settings.pauseOnHeadphonesDisconnect.flow.collectAsStateWithLifecycle() + val seekBackDuration by context.symphony.settings.seekBackDuration.flow.collectAsStateWithLifecycle() + val seekForwardDuration by context.symphony.settings.seekForwardDuration.flow.collectAsStateWithLifecycle() + val gaplessPlayback by context.symphony.settings.gaplessPlayback.flow.collectAsStateWithLifecycle() Scaffold( modifier = Modifier.fillMaxSize(), diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/UpdateSettingsView.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/UpdateSettingsView.kt index b15ee1af..6e175ce6 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/UpdateSettingsView.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/UpdateSettingsView.kt @@ -18,7 +18,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -37,8 +36,8 @@ object UpdateSettingsViewRoute @Composable fun UpdateSettingsView(context: ViewContext) { val scrollState = rememberScrollState() - val checkForUpdates by context.symphony.settings.checkForUpdates.flow.collectAsState() - val showUpdateToast by context.symphony.settings.showUpdateToast.flow.collectAsState() + val checkForUpdates by context.symphony.settings.checkForUpdates.flow.collectAsStateWithLifecycle() + val showUpdateToast by context.symphony.settings.showUpdateToast.flow.collectAsStateWithLifecycle() Scaffold( modifier = Modifier.fillMaxSize(), From d04408b8f5044da0dfa008227d456d9a3de16188 Mon Sep 17 00:00:00 2001 From: Zyrouge Date: Sun, 15 Jun 2025 20:10:23 +0530 Subject: [PATCH 08/15] refactor: better file names & dao --- .../zyrouge/symphony/services/AppMeta.kt | 6 +- .../zyrouge/symphony/services/Settings.kt | 6 +- .../symphony/services/database/Database.kt | 2 + .../database/store/AlbumArtistMappingStore.kt | 4 +- .../store/AlbumComposerMappingStore.kt | 28 +-- .../database/store/AlbumSongMappingStore.kt | 48 ++-- .../services/database/store/AlbumStore.kt | 115 +++++---- .../database/store/ArtistSongMappingStore.kt | 48 ++-- .../services/database/store/ArtistStore.kt | 110 ++++---- .../store/ComposerSongMappingStore.kt | 48 ++-- .../services/database/store/ComposerStore.kt | 62 +++-- .../database/store/GenreSongMappingStore.kt | 28 +-- .../services/database/store/GenreStore.kt | 82 +++--- .../database/store/MediaTreeFolderStore.kt | 94 ++++--- .../database/store/MediaTreeLyricFileStore.kt | 46 +++- .../database/store/MediaTreeSongFileStore.kt | 56 ++++- .../store/PlaylistSongMappingStore.kt | 119 +++++---- .../services/database/store/PlaylistStore.kt | 75 +++--- .../database/store/SongArtworkIndexStore.kt | 30 ++- .../services/database/store/SongLyricStore.kt | 4 +- .../store/SongQueueSongMappingStore.kt | 120 ++++++--- .../services/database/store/SongQueueStore.kt | 56 ++++- .../services/database/store/SongStore.kt | 238 +++++++++--------- .../symphony/services/groove/MediaExposer.kt | 8 +- .../services/groove/entities/SongQueue.kt | 26 ++ .../groove/entities/SongQueueSongMapping.kt | 10 + .../zyrouge/symphony/services/radio/Radio.kt | 180 +++---------- .../symphony/services/radio/RadioQueue.kt | 206 ++++++--------- .../ui/components/AddToPlaylistDialog.kt | 2 +- .../ui/components/LazyColumnScrollBar.kt | 2 +- .../ui/components/LazyGridScrollBar.kt | 2 +- .../ui/components/LongPressCopyableText.kt | 4 +- .../ui/components/NowPlayingBottomBar.kt | 2 +- .../zyrouge/symphony/ui/components/Slider.kt | 8 +- .../ui/components/SongInformationDialog.kt | 8 +- .../symphony/ui/components/SongTreeList.kt | 16 +- .../ui/components/TopAppBarMinimalTitle.kt | 2 +- .../settings/ConsiderContributingTile.kt | 6 +- .../ui/components/settings/LinkTile.kt | 4 +- .../settings/MultiSystemFolderTile.kt | 4 +- .../ui/helpers/{Context.kt => ViewContext.kt} | 0 .../ui/view/{Album.kt => AlbumView.kt} | 5 +- .../ui/view/{Artist.kt => ArtistView.kt} | 1 + .../symphony/ui/view/{Base.kt => BaseView.kt} | 28 +-- .../ui/view/{Genre.kt => GenreView.kt} | 1 + .../symphony/ui/view/{Home.kt => HomeView.kt} | 25 +- .../ui/view/{Lyrics.kt => LyricsView.kt} | 5 +- .../view/{NowPlaying.kt => NowPlayingView.kt} | 10 +- .../ui/view/{Playlist.kt => PlaylistView.kt} | 0 .../ui/view/{Queue.kt => QueueView.kt} | 4 +- .../ui/view/{Search.kt => SearchView.kt} | 2 +- .../ui/view/{Settings.kt => SettingsView.kt} | 4 +- .../zyrouge/symphony/ui/view/home/Browser.kt | 1 + .../zyrouge/symphony/ui/view/home/Folders.kt | 2 +- .../zyrouge/symphony/ui/view/home/ForYou.kt | 6 +- ...lbumArtists.kt => HomeAlbumArtistsView.kt} | 3 +- .../home/{Albums.kt => HomeAlbumsView.kt} | 3 +- .../home/{Artists.kt => HomeArtistsView.kt} | 3 +- .../{Playlists.kt => HomePlaylistsView.kt} | 7 +- .../view/home/{Songs.kt => HomeSongsView.kt} | 3 +- .../ui/view/home/{Tree.kt => HomeTreeView.kt} | 2 +- .../{AppBar.kt => NowPlayingAppBar.kt} | 0 .../nowPlaying/{Body.kt => NowPlayingBody.kt} | 0 ...ottomBar.kt => NowPlayingBodyBottomBar.kt} | 0 ...odyContent.kt => NowPlayingBodyContent.kt} | 4 +- .../{BodyCover.kt => NowPlayingBodyCover.kt} | 0 ...Playing.kt => NowPlayingNothingPlaying.kt} | 6 +- ...itchDialog.kt => NowPlayingPitchDialog.kt} | 0 ...ialog.kt => NowPlayingSleepTimerDialog.kt} | 6 +- ...peedDialog.kt => NowPlayingSpeedDialog.kt} | 0 ...tingsView.kt => SettingsAppearanceView.kt} | 3 +- ...eSettingsView.kt => SettingsGrooveView.kt} | 4 +- ...ettingsView.kt => SettingsHomePageView.kt} | 2 +- ...tingsView.kt => SettingsMiniPlayerView.kt} | 2 +- ...tingsView.kt => SettingsNowPlayingView.kt} | 2 +- ...rSettingsView.kt => SettingsPlayerView.kt} | 2 +- ...eSettingsView.kt => SettingsUpdateView.kt} | 2 +- .../{ActivityUtils.kt => ActivityHelper.kt} | 2 +- .../{DurationUtils.kt => DurationHelper.kt} | 2 +- .../io/github/zyrouge/symphony/utils/Fuzzy.kt | 1 + .../io/github/zyrouge/symphony/utils/Http.kt | 2 +- .../zyrouge/symphony/utils/ImagePreserver.kt | 2 +- .../{RangeUtils.kt => RangeCalculator.kt} | 2 +- .../{StringListUtils.kt => StringSorter.kt} | 2 +- .../zyrouge/symphony/utils/StringUtils.kt | 3 - .../zyrouge/symphony/utils/TimedContent.kt | 2 +- .../symphony/utils/{ => builtin}/Float.kt | 2 +- .../symphony/utils/{ => builtin}/List.kt | 2 +- .../symphony/utils/{ => builtin}/Run.kt | 2 +- .../symphony/utils/{ => builtin}/Set.kt | 2 +- .../zyrouge/symphony/utils/builtin/String.kt | 12 + 91 files changed, 1134 insertions(+), 967 deletions(-) rename app/src/main/java/io/github/zyrouge/symphony/ui/helpers/{Context.kt => ViewContext.kt} (100%) rename app/src/main/java/io/github/zyrouge/symphony/ui/view/{Album.kt => AlbumView.kt} (98%) rename app/src/main/java/io/github/zyrouge/symphony/ui/view/{Artist.kt => ArtistView.kt} (99%) rename app/src/main/java/io/github/zyrouge/symphony/ui/view/{Base.kt => BaseView.kt} (88%) rename app/src/main/java/io/github/zyrouge/symphony/ui/view/{Genre.kt => GenreView.kt} (98%) rename app/src/main/java/io/github/zyrouge/symphony/ui/view/{Home.kt => HomeView.kt} (95%) rename app/src/main/java/io/github/zyrouge/symphony/ui/view/{Lyrics.kt => LyricsView.kt} (96%) rename app/src/main/java/io/github/zyrouge/symphony/ui/view/{NowPlaying.kt => NowPlayingView.kt} (93%) rename app/src/main/java/io/github/zyrouge/symphony/ui/view/{Playlist.kt => PlaylistView.kt} (100%) rename app/src/main/java/io/github/zyrouge/symphony/ui/view/{Queue.kt => QueueView.kt} (98%) rename app/src/main/java/io/github/zyrouge/symphony/ui/view/{Search.kt => SearchView.kt} (99%) rename app/src/main/java/io/github/zyrouge/symphony/ui/view/{Settings.kt => SettingsView.kt} (98%) rename app/src/main/java/io/github/zyrouge/symphony/ui/view/home/{AlbumArtists.kt => HomeAlbumArtistsView.kt} (91%) rename app/src/main/java/io/github/zyrouge/symphony/ui/view/home/{Albums.kt => HomeAlbumsView.kt} (91%) rename app/src/main/java/io/github/zyrouge/symphony/ui/view/home/{Artists.kt => HomeArtistsView.kt} (91%) rename app/src/main/java/io/github/zyrouge/symphony/ui/view/home/{Playlists.kt => HomePlaylistsView.kt} (95%) rename app/src/main/java/io/github/zyrouge/symphony/ui/view/home/{Songs.kt => HomeSongsView.kt} (91%) rename app/src/main/java/io/github/zyrouge/symphony/ui/view/home/{Tree.kt => HomeTreeView.kt} (95%) rename app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/{AppBar.kt => NowPlayingAppBar.kt} (100%) rename app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/{Body.kt => NowPlayingBody.kt} (100%) rename app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/{BottomBar.kt => NowPlayingBodyBottomBar.kt} (100%) rename app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/{BodyContent.kt => NowPlayingBodyContent.kt} (99%) rename app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/{BodyCover.kt => NowPlayingBodyCover.kt} (100%) rename app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/{NothingPlaying.kt => NowPlayingNothingPlaying.kt} (88%) rename app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/{PitchDialog.kt => NowPlayingPitchDialog.kt} (100%) rename app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/{SleepTimerDialog.kt => NowPlayingSleepTimerDialog.kt} (98%) rename app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/{SpeedDialog.kt => NowPlayingSpeedDialog.kt} (100%) rename app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/{AppearanceSettingsView.kt => SettingsAppearanceView.kt} (99%) rename app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/{GrooveSettingsView.kt => SettingsGrooveView.kt} (99%) rename app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/{HomePageSettingsView.kt => SettingsHomePageView.kt} (99%) rename app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/{MiniPlayerSettingsView.kt => SettingsMiniPlayerView.kt} (99%) rename app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/{NowPlayingSettingsView.kt => SettingsNowPlayingView.kt} (99%) rename app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/{PlayerSettingsView.kt => SettingsPlayerView.kt} (99%) rename app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/{UpdateSettingsView.kt => SettingsUpdateView.kt} (98%) rename app/src/main/java/io/github/zyrouge/symphony/utils/{ActivityUtils.kt => ActivityHelper.kt} (98%) rename app/src/main/java/io/github/zyrouge/symphony/utils/{DurationUtils.kt => DurationHelper.kt} (96%) rename app/src/main/java/io/github/zyrouge/symphony/utils/{RangeUtils.kt => RangeCalculator.kt} (94%) rename app/src/main/java/io/github/zyrouge/symphony/utils/{StringListUtils.kt => StringSorter.kt} (93%) delete mode 100644 app/src/main/java/io/github/zyrouge/symphony/utils/StringUtils.kt rename app/src/main/java/io/github/zyrouge/symphony/utils/{ => builtin}/Float.kt (54%) rename app/src/main/java/io/github/zyrouge/symphony/utils/{ => builtin}/List.kt (94%) rename app/src/main/java/io/github/zyrouge/symphony/utils/{ => builtin}/Run.kt (82%) rename app/src/main/java/io/github/zyrouge/symphony/utils/{ => builtin}/Set.kt (92%) create mode 100644 app/src/main/java/io/github/zyrouge/symphony/utils/builtin/String.kt diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/AppMeta.kt b/app/src/main/java/io/github/zyrouge/symphony/services/AppMeta.kt index d4db651c..a98829e9 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/AppMeta.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/AppMeta.kt @@ -1,7 +1,7 @@ package io.github.zyrouge.symphony.services import io.github.zyrouge.symphony.BuildConfig -import io.github.zyrouge.symphony.utils.HttpClient +import io.github.zyrouge.symphony.utils.DefaultHttpClient import io.github.zyrouge.symphony.utils.Logger import okhttp3.CacheControl import okhttp3.Request @@ -46,7 +46,7 @@ object AppMeta { .url(latestReleaseUrl) .cacheControl(CacheControl.FORCE_NETWORK) .build() - val res = HttpClient.newCall(req).execute() + val res = DefaultHttpClient.newCall(req).execute() val content = res.body?.string() ?: "" val json = JSONObject(content) val tagName = json.getString("tag_name") @@ -68,7 +68,7 @@ object AppMeta { .url(latestReleaseUrl) .cacheControl(CacheControl.FORCE_NETWORK) .build() - val res = HttpClient.newCall(req).execute() + val res = DefaultHttpClient.newCall(req).execute() val content = res.body?.string() ?: "" val json = JSONArray(content) for (i in 0 until json.length()) { diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/Settings.kt b/app/src/main/java/io/github/zyrouge/symphony/services/Settings.kt index 05f78120..999f3332 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/Settings.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/Settings.kt @@ -19,7 +19,7 @@ import io.github.zyrouge.symphony.ui.view.NowPlayingControlsLayout import io.github.zyrouge.symphony.ui.view.NowPlayingLyricsLayout import io.github.zyrouge.symphony.ui.view.home.ForYou import io.github.zyrouge.symphony.utils.ImagePreserver -import io.github.zyrouge.symphony.utils.StringListUtils +import io.github.zyrouge.symphony.utils.StringSorter import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update @@ -234,8 +234,8 @@ class Settings(private val symphony: Symphony) { val lastUsedArtistSongsSortReverse = BooleanEntry("last_used_artist_songs_sort_reverse", false) val lastUsedTreePathSortBy = EnumEntry( "last_used_tree_path_sort_by", - enumEntries(), - StringListUtils.SortBy.NAME, + enumEntries(), + StringSorter.SortBy.NAME, ) val lastUsedTreePathSortReverse = BooleanEntry("last_used_tree_path_sort_reverse", false) val lastUsedFoldersHorizontalGridColumns = IntEntry( diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/Database.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/Database.kt index e76a5321..40e5c554 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/Database.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/Database.kt @@ -17,6 +17,8 @@ class Database(symphony: Symphony) { val mediaTreeLyricFilesIdGenerator = KeyGenerator.TimeCounterRandomMix() val playlistsIdGenerator = KeyGenerator.TimeCounterRandomMix() val playlistSongMappingIdGenerator = KeyGenerator.TimeCounterRandomMix() + val songQueueSongMappingIdGenerator = KeyGenerator.TimeCounterRandomMix() + val songQueueIdGenerator = KeyGenerator.TimeCounterRandomMix() val albums get() = persistent.albums() val albumArtistMapping get() = persistent.albumArtistMapping() diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumArtistMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumArtistMappingStore.kt index d1d340ef..da19eaa0 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumArtistMappingStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumArtistMappingStore.kt @@ -6,7 +6,7 @@ import androidx.room.OnConflictStrategy import io.github.zyrouge.symphony.services.groove.entities.AlbumArtistMapping @Dao -interface AlbumArtistMappingStore { +abstract class AlbumArtistMappingStore { @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun upsert(vararg entities: AlbumArtistMapping) + abstract suspend fun upsert(vararg entities: AlbumArtistMapping) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumComposerMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumComposerMappingStore.kt index 0fb1ab56..e4ae1745 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumComposerMappingStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumComposerMappingStore.kt @@ -7,19 +7,19 @@ import io.github.zyrouge.symphony.services.groove.entities.AlbumComposerMapping import io.github.zyrouge.symphony.services.groove.repositories.SongRepository @Dao -interface AlbumComposerMappingStore { +abstract class AlbumComposerMappingStore { @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun upsert(vararg entities: AlbumComposerMapping) -} + abstract suspend fun upsert(vararg entities: AlbumComposerMapping) -fun AlbumComposerMappingStore.valuesMappedAsFlow( - songStore: SongStore, - id: String, - sortBy: SongRepository.SortBy, - sortReverse: Boolean, -) = songStore.valuesAsFlow( - sortBy, - sortReverse, - additionalClauseBeforeJoins = "JOIN ${AlbumComposerMapping.TABLE}.${AlbumComposerMapping.COLUMN_COMPOSER_ID} = ? ", - additionalArgsBeforeJoins = arrayOf(id), -) + fun valuesMappedAsFlow( + songStore: SongStore, + id: String, + sortBy: SongRepository.SortBy, + sortReverse: Boolean, + ) = songStore.valuesAsFlow( + sortBy, + sortReverse, + additionalClauseBeforeJoins = "JOIN ${AlbumComposerMapping.TABLE}.${AlbumComposerMapping.COLUMN_COMPOSER_ID} = ? ", + additionalArgsBeforeJoins = arrayOf(id), + ) +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumSongMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumSongMappingStore.kt index 831c0e4b..a16de5c8 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumSongMappingStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumSongMappingStore.kt @@ -11,33 +11,33 @@ import io.github.zyrouge.symphony.services.groove.repositories.SongRepository import kotlinx.coroutines.flow.Flow @Dao -interface AlbumSongMappingStore { +abstract class AlbumSongMappingStore { @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun upsert(vararg entities: AlbumSongMapping) + abstract suspend fun upsert(vararg entities: AlbumSongMapping) @RawQuery(observedEntities = [SongArtworkIndex::class, AlbumSongMapping::class]) - fun findTop4SongArtworksAsFlowRaw(query: SimpleSQLiteQuery): Flow> -} + protected abstract fun findTop4SongArtworksAsFlow(query: SimpleSQLiteQuery): Flow> -fun AlbumSongMappingStore.valuesMappedAsFlow( - songStore: SongStore, - id: String, - sortBy: SongRepository.SortBy, - sortReverse: Boolean, -) = songStore.valuesAsFlow( - sortBy, - sortReverse, - additionalClauseBeforeJoins = "JOIN ${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_ALBUM_ID} = ? ", - additionalArgsBeforeJoins = arrayOf(id), -) + fun findTop4SongArtworksAsFlow(albumId: String): Flow> { + val query = "SELECT ${SongArtworkIndex.TABLE}.* " + + "FROM ${SongArtworkIndex.TABLE} " + + "LEFT JOIN ${AlbumSongMapping.TABLE} ON ${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_ALBUM_ID} = ? AND ${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_SONG_ID} = ${SongArtworkIndex.TABLE}.${SongArtworkIndex.COLUMN_SONG_ID} " + + "WHERE ${SongArtworkIndex.TABLE}.${SongArtworkIndex.COLUMN_FILE} != null " + + "ORDER BY DESC " + + "LIMIT 4" + val args = arrayOf(albumId) + return findTop4SongArtworksAsFlow(SimpleSQLiteQuery(query, args)) + } -fun AlbumSongMappingStore.findTop4SongArtworksAsFlow(albumId: String): Flow> { - val query = "SELECT ${SongArtworkIndex.TABLE}.* " + - "FROM ${SongArtworkIndex.TABLE} " + - "LEFT JOIN ${AlbumSongMapping.TABLE} ON ${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_ALBUM_ID} = ? AND ${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_SONG_ID} = ${SongArtworkIndex.TABLE}.${SongArtworkIndex.COLUMN_SONG_ID} " + - "WHERE ${SongArtworkIndex.TABLE}.${SongArtworkIndex.COLUMN_FILE} != null " + - "ORDER BY DESC " + - "LIMIT 4" - val args = arrayOf(albumId) - return findTop4SongArtworksAsFlowRaw(SimpleSQLiteQuery(query, args)) + fun valuesMappedAsFlow( + songStore: SongStore, + id: String, + sortBy: SongRepository.SortBy, + sortReverse: Boolean, + ) = songStore.valuesAsFlow( + sortBy, + sortReverse, + additionalClauseBeforeJoins = "JOIN ${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_ALBUM_ID} = ? ", + additionalArgsBeforeJoins = arrayOf(id), + ) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumStore.kt index a2b25336..b2125b72 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumStore.kt @@ -2,7 +2,6 @@ package io.github.zyrouge.symphony.services.database.store import androidx.room.Dao import androidx.room.Insert -import androidx.room.Query import androidx.room.RawQuery import androidx.room.Update import androidx.sqlite.db.SimpleSQLiteQuery @@ -15,68 +14,74 @@ import io.github.zyrouge.symphony.services.groove.repositories.AlbumRepository import kotlinx.coroutines.flow.Flow @Dao -interface AlbumStore { +abstract class AlbumStore { @Insert - suspend fun insert(vararg entities: Album): List + abstract suspend fun insert(vararg entities: Album): List @Update - suspend fun update(vararg entities: Album): Int + abstract suspend fun update(vararg entities: Album): Int - @Query("SELECT * FROM ${Album.TABLE} WHERE ${Album.COLUMN_NAME} = :name LIMIT 1") - fun findByName(name: String): Album? + @RawQuery + protected abstract fun findByName(query: SimpleSQLiteQuery): Album? - @RawQuery(observedEntities = [Album::class, AlbumArtistMapping::class, AlbumSongMapping::class]) - fun findByIdAsFlowRaw(query: SupportSQLiteQuery): Flow + fun findByName(name: String): Album? { + val query = "SELECT * FROM ${Album.TABLE} WHERE ${Album.COLUMN_NAME} = ? LIMIT 1" + val args = arrayOf(name) + return findByName(SimpleSQLiteQuery(query, args)) + } @RawQuery(observedEntities = [Album::class, AlbumArtistMapping::class, AlbumSongMapping::class]) - fun valuesAsFlowRaw(query: SupportSQLiteQuery): Flow> -} + protected abstract fun findByIdAsFlow(query: SupportSQLiteQuery): Flow -fun AlbumStore.findByIdAsFlow(id: String): Flow { - val query = "SELECT ${Album.TABLE}.*, " + - "COUNT(${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_SONG_ID}) as ${Album.AlongAttributes.EMBEDDED_TRACKS_COUNT}, " + - "COUNT(${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ARTIST_ID}) as ${Album.AlongAttributes.EMBEDDED_ARTISTS_COUNT} " + - "FROM ${Album.TABLE} " + - "LEFT JOIN ${AlbumSongMapping.TABLE} ON ${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_ALBUM_ID} = ${Album.TABLE}.${Album.COLUMN_ID} " + - "LEFT JOIN ${AlbumArtistMapping.TABLE} ON ${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID} = ${Album.TABLE}.${Album.COLUMN_ID} " + - "WHERE ${Album.COLUMN_ID} = ? " + - "LIMIT 1" - val args = arrayOf(id) - return findByIdAsFlowRaw(SimpleSQLiteQuery(query, args)) -} + fun findByIdAsFlow(id: String): Flow { + val query = "SELECT ${Album.TABLE}.*, " + + "COUNT(${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_SONG_ID}) as ${Album.AlongAttributes.EMBEDDED_TRACKS_COUNT}, " + + "COUNT(${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ARTIST_ID}) as ${Album.AlongAttributes.EMBEDDED_ARTISTS_COUNT} " + + "FROM ${Album.TABLE} " + + "LEFT JOIN ${AlbumSongMapping.TABLE} ON ${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_ALBUM_ID} = ${Album.TABLE}.${Album.COLUMN_ID} " + + "LEFT JOIN ${AlbumArtistMapping.TABLE} ON ${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID} = ${Album.TABLE}.${Album.COLUMN_ID} " + + "WHERE ${Album.COLUMN_ID} = ? " + + "LIMIT 1" + val args = arrayOf(id) + return findByIdAsFlow(SimpleSQLiteQuery(query, args)) + } + + @RawQuery(observedEntities = [Album::class, AlbumArtistMapping::class, AlbumSongMapping::class]) + protected abstract fun valuesAsFlow(query: SupportSQLiteQuery): Flow> -fun AlbumStore.valuesAsFlow( - sortBy: AlbumRepository.SortBy, - sortReverse: Boolean, - artistId: String? = null, -): Flow> { - val aliasFirstArtist = "firstArtist" - val embeddedArtistName = "firstArtistName" - val orderBy = when (sortBy) { - AlbumRepository.SortBy.CUSTOM -> "${Album.TABLE}.${Album.COLUMN_ID}" - AlbumRepository.SortBy.ALBUM_NAME -> "${Album.TABLE}.${Album.COLUMN_NAME}" - AlbumRepository.SortBy.ARTIST_NAME -> embeddedArtistName - AlbumRepository.SortBy.YEAR -> "${Album.TABLE}.${Album.COLUMN_START_YEAR}" - AlbumRepository.SortBy.TRACKS_COUNT -> Album.AlongAttributes.EMBEDDED_TRACKS_COUNT - AlbumRepository.SortBy.ARTISTS_COUNT -> Album.AlongAttributes.EMBEDDED_ARTISTS_COUNT + fun valuesAsFlow( + sortBy: AlbumRepository.SortBy, + sortReverse: Boolean, + artistId: String? = null, + ): Flow> { + val aliasFirstArtist = "firstArtist" + val embeddedArtistName = "firstArtistName" + val orderBy = when (sortBy) { + AlbumRepository.SortBy.CUSTOM -> "${Album.TABLE}.${Album.COLUMN_ID}" + AlbumRepository.SortBy.ALBUM_NAME -> "${Album.TABLE}.${Album.COLUMN_NAME}" + AlbumRepository.SortBy.ARTIST_NAME -> embeddedArtistName + AlbumRepository.SortBy.YEAR -> "${Album.TABLE}.${Album.COLUMN_START_YEAR}" + AlbumRepository.SortBy.TRACKS_COUNT -> Album.AlongAttributes.EMBEDDED_TRACKS_COUNT + AlbumRepository.SortBy.ARTISTS_COUNT -> Album.AlongAttributes.EMBEDDED_ARTISTS_COUNT + } + val orderDirection = if (sortReverse) "DESC" else "ASC" + val artistQuery = "SELECT" + + "TOP 1 ${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ARTIST_ID} " + + "FROM ${AlbumArtistMapping.TABLE} " + + "WHERE ${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID} = ${Album.COLUMN_ID} " + + "ORDER BY ${AlbumArtistMapping.COLUMN_IS_ALBUM_ARTIST} DESC" + val albumArtistMappingJoin = "" + + (if (artistId != null) "${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ARTIST_ID} = ? " else "") + + "${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID} = ${Album.TABLE}.${Album.COLUMN_ID}" + val query = "SELECT ${Album.TABLE}.*, " + + "COUNT(${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_SONG_ID}) as ${Album.AlongAttributes.EMBEDDED_TRACKS_COUNT}, " + + "COUNT(${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ARTIST_ID}) as ${Album.AlongAttributes.EMBEDDED_ARTISTS_COUNT}, " + + "$aliasFirstArtist.${Artist.COLUMN_NAME} as $embeddedArtistName" + + "FROM ${Album.TABLE} " + + "LEFT JOIN ${AlbumSongMapping.TABLE} ON ${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_ALBUM_ID} = ${Album.TABLE}.${Album.COLUMN_ID} " + + "LEFT JOIN ${AlbumArtistMapping.TABLE} ON $albumArtistMappingJoin " + + "LEFT JOIN ${Artist.TABLE} $aliasFirstArtist ON ${Artist.TABLE}.${Artist.COLUMN_ID} = ($artistQuery) " + + "ORDER BY $orderBy $orderDirection" + return valuesAsFlow(SimpleSQLiteQuery(query)) } - val orderDirection = if (sortReverse) "DESC" else "ASC" - val artistQuery = "SELECT" + - "TOP 1 ${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ARTIST_ID} " + - "FROM ${AlbumArtistMapping.TABLE} " + - "WHERE ${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID} = ${Album.COLUMN_ID} " + - "ORDER BY ${AlbumArtistMapping.COLUMN_IS_ALBUM_ARTIST} DESC" - val albumArtistMappingJoin = "" + - (if (artistId != null) "${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ARTIST_ID} = ? " else "") + - "${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID} = ${Album.TABLE}.${Album.COLUMN_ID}" - val query = "SELECT ${Album.TABLE}.*, " + - "COUNT(${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_SONG_ID}) as ${Album.AlongAttributes.EMBEDDED_TRACKS_COUNT}, " + - "COUNT(${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ARTIST_ID}) as ${Album.AlongAttributes.EMBEDDED_ARTISTS_COUNT}, " + - "$aliasFirstArtist.${Artist.COLUMN_NAME} as $embeddedArtistName" + - "FROM ${Album.TABLE} " + - "LEFT JOIN ${AlbumSongMapping.TABLE} ON ${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_ALBUM_ID} = ${Album.TABLE}.${Album.COLUMN_ID} " + - "LEFT JOIN ${AlbumArtistMapping.TABLE} ON $albumArtistMappingJoin " + - "LEFT JOIN ${Artist.TABLE} $aliasFirstArtist ON ${Artist.TABLE}.${Artist.COLUMN_ID} = ($artistQuery) " + - "ORDER BY $orderBy $orderDirection" - return valuesAsFlowRaw(SimpleSQLiteQuery(query)) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistSongMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistSongMappingStore.kt index a70bbaf3..38857224 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistSongMappingStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistSongMappingStore.kt @@ -11,33 +11,33 @@ import io.github.zyrouge.symphony.services.groove.repositories.SongRepository import kotlinx.coroutines.flow.Flow @Dao -interface ArtistSongMappingStore { +abstract class ArtistSongMappingStore { @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun upsert(vararg entities: ArtistSongMapping) + abstract suspend fun upsert(vararg entities: ArtistSongMapping) @RawQuery(observedEntities = [SongArtworkIndex::class, ArtistSongMapping::class]) - fun findTop4SongArtworksAsFlowRaw(query: SimpleSQLiteQuery): Flow> -} + protected abstract fun findTop4SongArtworksAsFlow(query: SimpleSQLiteQuery): Flow> -fun ArtistSongMappingStore.valuesMappedAsFlow( - songStore: SongStore, - id: String, - sortBy: SongRepository.SortBy, - sortReverse: Boolean, -) = songStore.valuesAsFlow( - sortBy, - sortReverse, - additionalClauseBeforeJoins = "JOIN ${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_ARTIST_ID} = ? ", - additionalArgsBeforeJoins = arrayOf(id), -) + fun findTop4SongArtworksAsFlow(artistId: String): Flow> { + val query = "SELECT ${SongArtworkIndex.TABLE}.* " + + "FROM ${SongArtworkIndex.TABLE} " + + "LEFT JOIN ${ArtistSongMapping.TABLE} ON ${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_ARTIST_ID} = ? AND ${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_SONG_ID} = ${SongArtworkIndex.TABLE}.${SongArtworkIndex.COLUMN_SONG_ID} " + + "WHERE ${SongArtworkIndex.TABLE}.${SongArtworkIndex.COLUMN_FILE} != null " + + "ORDER BY DESC " + + "LIMIT 4" + val args = arrayOf(artistId) + return findTop4SongArtworksAsFlow(SimpleSQLiteQuery(query, args)) + } -fun ArtistSongMappingStore.findTop4SongArtworksAsFlow(artistId: String): Flow> { - val query = "SELECT ${SongArtworkIndex.TABLE}.* " + - "FROM ${SongArtworkIndex.TABLE} " + - "LEFT JOIN ${ArtistSongMapping.TABLE} ON ${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_ARTIST_ID} = ? AND ${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_SONG_ID} = ${SongArtworkIndex.TABLE}.${SongArtworkIndex.COLUMN_SONG_ID} " + - "WHERE ${SongArtworkIndex.TABLE}.${SongArtworkIndex.COLUMN_FILE} != null " + - "ORDER BY DESC " + - "LIMIT 4" - val args = arrayOf(artistId) - return findTop4SongArtworksAsFlowRaw(SimpleSQLiteQuery(query, args)) + fun valuesMappedAsFlow( + songStore: SongStore, + id: String, + sortBy: SongRepository.SortBy, + sortReverse: Boolean, + ) = songStore.valuesAsFlow( + sortBy, + sortReverse, + additionalClauseBeforeJoins = "JOIN ${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_ARTIST_ID} = ? ", + additionalArgsBeforeJoins = arrayOf(id), + ) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistStore.kt index ae67ee7b..f10f803a 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistStore.kt @@ -3,7 +3,6 @@ package io.github.zyrouge.symphony.services.database.store import androidx.room.Dao import androidx.room.Insert import androidx.room.MapColumn -import androidx.room.Query import androidx.room.RawQuery import androidx.room.Update import androidx.sqlite.db.SimpleSQLiteQuery @@ -13,68 +12,79 @@ import io.github.zyrouge.symphony.services.groove.entities.AlbumSongMapping import io.github.zyrouge.symphony.services.groove.entities.Artist import io.github.zyrouge.symphony.services.groove.entities.ArtistSongMapping import io.github.zyrouge.symphony.services.groove.repositories.ArtistRepository +import io.github.zyrouge.symphony.utils.builtin.sqlqph import kotlinx.coroutines.flow.Flow @Dao -interface ArtistStore { +abstract class ArtistStore { @Insert - suspend fun insert(vararg entities: Artist): List + abstract suspend fun insert(vararg entities: Artist): List @Update - suspend fun update(vararg entities: Artist): Int + abstract suspend fun update(vararg entities: Artist): Int @RawQuery(observedEntities = [Artist::class, ArtistSongMapping::class, AlbumArtistMapping::class]) - fun findByIdAsFlowRaw(query: SupportSQLiteQuery): Flow + protected abstract fun findByIdAsFlow(query: SupportSQLiteQuery): Flow + + fun findByIdAsFlow(id: String): Flow { + val query = "SELECT ${Artist.TABLE}.*, " + + "COUNT(${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_SONG_ID}) as ${Artist.AlongAttributes.EMBEDDED_TRACKS_COUNT}, " + + "COUNT(${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID}) as ${Artist.AlongAttributes.EMBEDDED_ALBUMS_COUNT} " + + "FROM ${Artist.TABLE} " + + "LEFT JOIN ${AlbumSongMapping.TABLE} ON ${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_ALBUM_ID} = ${Artist.TABLE}.${Artist.COLUMN_ID} " + + "LEFT JOIN ${AlbumArtistMapping.TABLE} ON ${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID} = ${Artist.TABLE}.${Artist.COLUMN_ID} " + + "WHERE ${Artist.COLUMN_ID} = ? " + + "LIMIT 1" + val args = arrayOf(id) + return findByIdAsFlow(SimpleSQLiteQuery(query, args)) + } @RawQuery(observedEntities = [Artist::class, ArtistSongMapping::class, AlbumArtistMapping::class]) - fun valuesAsFlowRaw(query: SupportSQLiteQuery): Flow> + protected abstract fun valuesAsFlow(query: SupportSQLiteQuery): Flow> - @Query("SELECT ${Artist.COLUMN_ID}, ${Artist.COLUMN_NAME} FROM ${Artist.TABLE} WHERE ${Artist.COLUMN_NAME} in (:names)") - fun entriesByNameNameIdMapped(names: Collection): Map< + fun valuesAsFlow( + sortBy: ArtistRepository.SortBy, + sortReverse: Boolean, + albumId: String? = null, + onlyAlbumArtists: Boolean = false, + ): Flow> { + val orderBy = when (sortBy) { + ArtistRepository.SortBy.CUSTOM -> "${Artist.TABLE}.${Artist.COLUMN_ID}" + ArtistRepository.SortBy.ARTIST_NAME -> "${Artist.TABLE}.${Artist.COLUMN_NAME}" + ArtistRepository.SortBy.TRACKS_COUNT -> Artist.AlongAttributes.EMBEDDED_TRACKS_COUNT + ArtistRepository.SortBy.ALBUMS_COUNT -> Artist.AlongAttributes.EMBEDDED_ALBUMS_COUNT + } + val orderDirection = if (sortReverse) "DESC" else "ASC" + val albumArtistMappingJoin = "" + + (if (albumId != null) "${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID} = ? " else "") + + (if (onlyAlbumArtists) "${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_IS_ALBUM_ARTIST} = 1 " else "") + + "${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ARTIST_ID} = ${Artist.TABLE}.${Artist.COLUMN_ID}" + val query = "SELECT ${Artist.TABLE}.*, " + + "COUNT(${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_SONG_ID}) as ${Artist.AlongAttributes.EMBEDDED_TRACKS_COUNT}, " + + "COUNT(${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID}) as ${Artist.AlongAttributes.EMBEDDED_ALBUMS_COUNT} " + + "FROM ${Artist.TABLE} " + + "LEFT JOIN ${ArtistSongMapping.TABLE} ON ${ArtistSongMapping.TABLE}.${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_ARTIST_ID} = ${Artist.TABLE}.${Artist.COLUMN_ID} " + + "LEFT JOIN ${AlbumArtistMapping.TABLE} ON $albumArtistMappingJoin " + + "ORDER BY $orderBy $orderDirection" + val args = mutableListOf() + if (albumId != null) { + args.add(albumId) + } + return valuesAsFlow(SimpleSQLiteQuery(query, args.toTypedArray())) + } + + @RawQuery + protected abstract fun entriesByNameNameIdMapped(query: SimpleSQLiteQuery): Map< @MapColumn(Artist.COLUMN_NAME) String, @MapColumn(Artist.COLUMN_ID) String> -} -fun ArtistStore.findByIdAsFlow(id: String): Flow { - val query = "SELECT ${Artist.TABLE}.*, " + - "COUNT(${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_SONG_ID}) as ${Artist.AlongAttributes.EMBEDDED_TRACKS_COUNT}, " + - "COUNT(${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID}) as ${Artist.AlongAttributes.EMBEDDED_ALBUMS_COUNT} " + - "FROM ${Artist.TABLE} " + - "LEFT JOIN ${AlbumSongMapping.TABLE} ON ${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_ALBUM_ID} = ${Artist.TABLE}.${Artist.COLUMN_ID} " + - "LEFT JOIN ${AlbumArtistMapping.TABLE} ON ${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID} = ${Artist.TABLE}.${Artist.COLUMN_ID} " + - "WHERE ${Artist.COLUMN_ID} = ? " + - "LIMIT 1" - val args = arrayOf(id) - return findByIdAsFlowRaw(SimpleSQLiteQuery(query, args)) -} - -fun ArtistStore.valuesAsFlow( - sortBy: ArtistRepository.SortBy, - sortReverse: Boolean, - albumId: String? = null, - onlyAlbumArtists: Boolean = false, -): Flow> { - val orderBy = when (sortBy) { - ArtistRepository.SortBy.CUSTOM -> "${Artist.TABLE}.${Artist.COLUMN_ID}" - ArtistRepository.SortBy.ARTIST_NAME -> "${Artist.TABLE}.${Artist.COLUMN_NAME}" - ArtistRepository.SortBy.TRACKS_COUNT -> Artist.AlongAttributes.EMBEDDED_TRACKS_COUNT - ArtistRepository.SortBy.ALBUMS_COUNT -> Artist.AlongAttributes.EMBEDDED_ALBUMS_COUNT - } - val orderDirection = if (sortReverse) "DESC" else "ASC" - val albumArtistMappingJoin = "" + - (if (albumId != null) "${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID} = ? " else "") + - (if (onlyAlbumArtists) "${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_IS_ALBUM_ARTIST} = 1 " else "") + - "${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ARTIST_ID} = ${Artist.TABLE}.${Artist.COLUMN_ID}" - val query = "SELECT ${Artist.TABLE}.*, " + - "COUNT(${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_SONG_ID}) as ${Artist.AlongAttributes.EMBEDDED_TRACKS_COUNT}, " + - "COUNT(${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID}) as ${Artist.AlongAttributes.EMBEDDED_ALBUMS_COUNT} " + - "FROM ${Artist.TABLE} " + - "LEFT JOIN ${ArtistSongMapping.TABLE} ON ${ArtistSongMapping.TABLE}.${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_ARTIST_ID} = ${Artist.TABLE}.${Artist.COLUMN_ID} " + - "LEFT JOIN ${AlbumArtistMapping.TABLE} ON $albumArtistMappingJoin " + - "ORDER BY $orderBy $orderDirection" - val args = mutableListOf() - if (albumId != null) { - args.add(albumId) + fun entriesByNameNameIdMapped(names: Collection): Map< + @MapColumn(Artist.COLUMN_NAME) String, + @MapColumn(Artist.COLUMN_ID) String> { + val query = "SELECT ${Artist.COLUMN_ID}, ${Artist.COLUMN_NAME} " + + "FROM ${Artist.TABLE} " + + "WHERE ${Artist.COLUMN_NAME} in (${sqlqph(names.size)})" + val args = arrayOf(*names.toTypedArray()) + return entriesByNameNameIdMapped(SimpleSQLiteQuery(query, args)) } - return valuesAsFlowRaw(SimpleSQLiteQuery(query, args.toTypedArray())) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ComposerSongMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ComposerSongMappingStore.kt index 080174ee..a0ba1743 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ComposerSongMappingStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ComposerSongMappingStore.kt @@ -11,33 +11,33 @@ import io.github.zyrouge.symphony.services.groove.repositories.SongRepository import kotlinx.coroutines.flow.Flow @Dao -interface ComposerSongMappingStore { +abstract class ComposerSongMappingStore { @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun upsert(vararg entities: ComposerSongMapping) + abstract suspend fun upsert(vararg entities: ComposerSongMapping) @RawQuery(observedEntities = [SongArtworkIndex::class, ComposerSongMapping::class]) - fun findTop4SongArtworksAsFlowRaw(query: SimpleSQLiteQuery): Flow> -} + protected abstract fun findTop4SongArtworksAsFlow(query: SimpleSQLiteQuery): Flow> -fun ComposerSongMappingStore.valuesMappedAsFlow( - songStore: SongStore, - id: String, - sortBy: SongRepository.SortBy, - sortReverse: Boolean, -) = songStore.valuesAsFlow( - sortBy, - sortReverse, - additionalClauseBeforeJoins = "JOIN ${ComposerSongMapping.TABLE}.${ComposerSongMapping.COLUMN_COMPOSER_ID} = ? ", - additionalArgsBeforeJoins = arrayOf(id), -) + fun findTop4SongArtworksAsFlow(composerId: String): Flow> { + val query = "SELECT ${SongArtworkIndex.TABLE}.* " + + "FROM ${SongArtworkIndex.TABLE} " + + "LEFT JOIN ${ComposerSongMapping.TABLE} ON ${ComposerSongMapping.TABLE}.${ComposerSongMapping.COLUMN_COMPOSER_ID} = ? AND ${ComposerSongMapping.TABLE}.${ComposerSongMapping.COLUMN_SONG_ID} = ${SongArtworkIndex.TABLE}.${SongArtworkIndex.COLUMN_SONG_ID} " + + "WHERE ${SongArtworkIndex.TABLE}.${SongArtworkIndex.COLUMN_FILE} != null " + + "ORDER BY DESC " + + "LIMIT 4" + val args = arrayOf(composerId) + return findTop4SongArtworksAsFlow(SimpleSQLiteQuery(query, args)) + } -fun ComposerSongMappingStore.findTop4SongArtworksAsFlow(composerId: String): Flow> { - val query = "SELECT ${SongArtworkIndex.TABLE}.* " + - "FROM ${SongArtworkIndex.TABLE} " + - "LEFT JOIN ${ComposerSongMapping.TABLE} ON ${ComposerSongMapping.TABLE}.${ComposerSongMapping.COLUMN_COMPOSER_ID} = ? AND ${ComposerSongMapping.TABLE}.${ComposerSongMapping.COLUMN_SONG_ID} = ${SongArtworkIndex.TABLE}.${SongArtworkIndex.COLUMN_SONG_ID} " + - "WHERE ${SongArtworkIndex.TABLE}.${SongArtworkIndex.COLUMN_FILE} != null " + - "ORDER BY DESC " + - "LIMIT 4" - val args = arrayOf(composerId) - return findTop4SongArtworksAsFlowRaw(SimpleSQLiteQuery(query, args)) + fun valuesMappedAsFlow( + songStore: SongStore, + id: String, + sortBy: SongRepository.SortBy, + sortReverse: Boolean, + ) = songStore.valuesAsFlow( + sortBy, + sortReverse, + additionalClauseBeforeJoins = "JOIN ${ComposerSongMapping.TABLE}.${ComposerSongMapping.COLUMN_COMPOSER_ID} = ? ", + additionalArgsBeforeJoins = arrayOf(id), + ) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ComposerStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ComposerStore.kt index 1819d8e9..4a777524 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ComposerStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ComposerStore.kt @@ -3,7 +3,6 @@ package io.github.zyrouge.symphony.services.database.store import androidx.room.Dao import androidx.room.Insert import androidx.room.MapColumn -import androidx.room.Query import androidx.room.RawQuery import androidx.room.Update import androidx.sqlite.db.SimpleSQLiteQuery @@ -12,42 +11,53 @@ import io.github.zyrouge.symphony.services.groove.entities.AlbumComposerMapping import io.github.zyrouge.symphony.services.groove.entities.Composer import io.github.zyrouge.symphony.services.groove.entities.ComposerSongMapping import io.github.zyrouge.symphony.services.groove.repositories.ComposerRepository +import io.github.zyrouge.symphony.utils.builtin.sqlqph import kotlinx.coroutines.flow.Flow @Dao -interface ComposerStore { +abstract class ComposerStore { @Insert - suspend fun insert(vararg entities: Composer): List + abstract suspend fun insert(vararg entities: Composer): List @Update - suspend fun update(vararg entities: Composer): Int + abstract suspend fun update(vararg entities: Composer): Int @RawQuery(observedEntities = [Composer::class, ComposerSongMapping::class, AlbumComposerMapping::class]) - fun valuesAsFlowRaw(query: SupportSQLiteQuery): Flow> + protected abstract fun valuesAsFlow(query: SupportSQLiteQuery): Flow> - @Query("SELECT ${Composer.COLUMN_ID}, ${Composer.COLUMN_NAME} FROM ${Composer.TABLE} WHERE ${Composer.COLUMN_NAME} in (:names)") - fun entriesByNameNameIdMapped(names: Collection): Map< + fun valuesAsFlow( + sortBy: ComposerRepository.SortBy, + sortReverse: Boolean, + ): Flow> { + val orderBy = when (sortBy) { + ComposerRepository.SortBy.CUSTOM -> "${Composer.TABLE}.${Composer.COLUMN_ID}" + ComposerRepository.SortBy.COMPOSER_NAME -> "${Composer.TABLE}.${Composer.COLUMN_NAME}" + ComposerRepository.SortBy.TRACKS_COUNT -> Composer.AlongAttributes.EMBEDDED_TRACKS_COUNT + ComposerRepository.SortBy.ALBUMS_COUNT -> Composer.AlongAttributes.EMBEDDED_ALBUMS_COUNT + } + val orderDirection = if (sortReverse) "DESC" else "ASC" + val query = "SELECT ${Composer.TABLE}.*, " + + "COUNT(${ComposerSongMapping.TABLE}.${ComposerSongMapping.COLUMN_SONG_ID}) as ${Composer.AlongAttributes.EMBEDDED_TRACKS_COUNT}, " + + "COUNT(${AlbumComposerMapping.TABLE}.${AlbumComposerMapping.COLUMN_ALBUM_ID}) as ${Composer.AlongAttributes.EMBEDDED_ALBUMS_COUNT} " + + "FROM ${Composer.TABLE} " + + "LEFT JOIN ${ComposerSongMapping.TABLE} ON ${ComposerSongMapping.TABLE}.${ComposerSongMapping.COLUMN_COMPOSER_ID} = ${Composer.TABLE}.${Composer.COLUMN_ID} " + + "LEFT JOIN ${AlbumComposerMapping.TABLE} ON ${AlbumComposerMapping.TABLE}.${AlbumComposerMapping.COLUMN_COMPOSER_ID} = ${Composer.TABLE}.${Composer.COLUMN_ID}" + + "ORDER BY $orderBy $orderDirection" + return valuesAsFlow(SimpleSQLiteQuery(query)) + } + + @RawQuery + protected abstract fun entriesByNameNameIdMapped(query: SimpleSQLiteQuery): Map< @MapColumn(Composer.COLUMN_NAME) String, @MapColumn(Composer.COLUMN_ID) String> -} -fun ComposerStore.valuesAsFlow( - sortBy: ComposerRepository.SortBy, - sortReverse: Boolean, -): Flow> { - val orderBy = when (sortBy) { - ComposerRepository.SortBy.CUSTOM -> "${Composer.TABLE}.${Composer.COLUMN_ID}" - ComposerRepository.SortBy.COMPOSER_NAME -> "${Composer.TABLE}.${Composer.COLUMN_NAME}" - ComposerRepository.SortBy.TRACKS_COUNT -> Composer.AlongAttributes.EMBEDDED_TRACKS_COUNT - ComposerRepository.SortBy.ALBUMS_COUNT -> Composer.AlongAttributes.EMBEDDED_ALBUMS_COUNT + fun entriesByNameNameIdMapped(names: Collection): Map< + @MapColumn(Composer.COLUMN_NAME) String, + @MapColumn(Composer.COLUMN_ID) String> { + val query = "SELECT ${Composer.COLUMN_ID}, ${Composer.COLUMN_NAME} " + + "FROM ${Composer.TABLE} " + + "WHERE ${Composer.COLUMN_NAME} IN (${sqlqph(names.size)})" + val args = arrayOf(*names.toTypedArray()) + return entriesByNameNameIdMapped(SimpleSQLiteQuery(query, args)) } - val orderDirection = if (sortReverse) "DESC" else "ASC" - val query = "SELECT ${Composer.TABLE}.*, " + - "COUNT(${ComposerSongMapping.TABLE}.${ComposerSongMapping.COLUMN_SONG_ID}) as ${Composer.AlongAttributes.EMBEDDED_TRACKS_COUNT}, " + - "COUNT(${AlbumComposerMapping.TABLE}.${AlbumComposerMapping.COLUMN_ALBUM_ID}) as ${Composer.AlongAttributes.EMBEDDED_ALBUMS_COUNT} " + - "FROM ${Composer.TABLE} " + - "LEFT JOIN ${ComposerSongMapping.TABLE} ON ${ComposerSongMapping.TABLE}.${ComposerSongMapping.COLUMN_COMPOSER_ID} = ${Composer.TABLE}.${Composer.COLUMN_ID} " + - "LEFT JOIN ${AlbumComposerMapping.TABLE} ON ${AlbumComposerMapping.TABLE}.${AlbumComposerMapping.COLUMN_COMPOSER_ID} = ${Composer.TABLE}.${Composer.COLUMN_ID}" + - "ORDER BY $orderBy $orderDirection" - return valuesAsFlowRaw(SimpleSQLiteQuery(query)) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreSongMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreSongMappingStore.kt index 64210ff1..35f7bb1d 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreSongMappingStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreSongMappingStore.kt @@ -7,19 +7,19 @@ import io.github.zyrouge.symphony.services.groove.entities.GenreSongMapping import io.github.zyrouge.symphony.services.groove.repositories.SongRepository @Dao -interface GenreSongMappingStore { +abstract class GenreSongMappingStore { @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun upsert(vararg entities: GenreSongMapping) -} + abstract suspend fun upsert(vararg entities: GenreSongMapping) -fun GenreSongMappingStore.valuesMappedAsFlow( - songStore: SongStore, - id: String, - sortBy: SongRepository.SortBy, - sortReverse: Boolean, -) = songStore.valuesAsFlow( - sortBy, - sortReverse, - additionalClauseBeforeJoins = "JOIN ${GenreSongMapping.TABLE}.${GenreSongMapping.COLUMN_GENRE_ID} = ? ", - additionalArgsBeforeJoins = arrayOf(id), -) + fun valuesMappedAsFlow( + songStore: SongStore, + id: String, + sortBy: SongRepository.SortBy, + sortReverse: Boolean, + ) = songStore.valuesAsFlow( + sortBy, + sortReverse, + additionalClauseBeforeJoins = "JOIN ${GenreSongMapping.TABLE}.${GenreSongMapping.COLUMN_GENRE_ID} = ? ", + additionalArgsBeforeJoins = arrayOf(id), + ) +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreStore.kt index 261ee0f3..15f720ce 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreStore.kt @@ -3,59 +3,69 @@ package io.github.zyrouge.symphony.services.database.store import androidx.room.Dao import androidx.room.Insert import androidx.room.MapColumn -import androidx.room.Query import androidx.room.RawQuery import androidx.sqlite.db.SimpleSQLiteQuery import androidx.sqlite.db.SupportSQLiteQuery import io.github.zyrouge.symphony.services.groove.entities.Genre import io.github.zyrouge.symphony.services.groove.entities.GenreSongMapping import io.github.zyrouge.symphony.services.groove.repositories.GenreRepository +import io.github.zyrouge.symphony.utils.builtin.sqlqph import kotlinx.coroutines.flow.Flow @Dao -interface GenreStore { +abstract class GenreStore { @Insert - suspend fun insert(vararg entities: Genre): List + abstract suspend fun insert(vararg entities: Genre): List @RawQuery(observedEntities = [Genre::class, GenreSongMapping::class]) - fun findByIdAsFlowRaw(query: SupportSQLiteQuery): Flow + protected abstract fun findByIdAsFlow(query: SupportSQLiteQuery): Flow + + fun findByIdAsFlow(id: String): Flow { + val query = "SELECT ${Genre.TABLE}.*, " + + "COUNT(${GenreSongMapping.TABLE}.${GenreSongMapping.COLUMN_SONG_ID}) as ${Genre.AlongAttributes.EMBEDDED_TRACKS_COUNT} " + + "FROM ${Genre.TABLE} " + + "LEFT JOIN ${GenreSongMapping.TABLE} ON ${GenreSongMapping.TABLE}.${GenreSongMapping.COLUMN_GENRE_ID} = ${Genre.TABLE}.${Genre.COLUMN_ID} " + + "LEFT JOIN ${GenreSongMapping.TABLE} ON ${GenreSongMapping.TABLE}.${GenreSongMapping.COLUMN_GENRE_ID} = ${Genre.TABLE}.${Genre.COLUMN_ID} " + + "WHERE ${Genre.COLUMN_ID} = ? " + + "LIMIT 1" + val args = arrayOf(id) + return findByIdAsFlow(SimpleSQLiteQuery(query, args)) + } @RawQuery(observedEntities = [Genre::class, GenreSongMapping::class]) - fun valuesAsFlowRaw(query: SupportSQLiteQuery): Flow> + protected abstract fun valuesAsFlow(query: SupportSQLiteQuery): Flow> - @Query("SELECT ${Genre.COLUMN_ID}, ${Genre.COLUMN_NAME} FROM ${Genre.TABLE} WHERE ${Genre.COLUMN_NAME} in (:names)") - fun entriesByNameNameIdMapped(names: Collection): Map< + fun valuesAsFlow( + sortBy: GenreRepository.SortBy, + sortReverse: Boolean, + ): Flow> { + val orderBy = when (sortBy) { + GenreRepository.SortBy.CUSTOM -> "${Genre.TABLE}.${Genre.COLUMN_ID}" + GenreRepository.SortBy.GENRE -> "${Genre.TABLE}.${Genre.COLUMN_NAME}" + GenreRepository.SortBy.TRACKS_COUNT -> Genre.AlongAttributes.EMBEDDED_TRACKS_COUNT + } + val orderDirection = if (sortReverse) "DESC" else "ASC" + val query = "SELECT ${Genre.TABLE}.*, " + + "COUNT(${GenreSongMapping.TABLE}.${GenreSongMapping.COLUMN_SONG_ID}) as ${Genre.AlongAttributes.EMBEDDED_TRACKS_COUNT} " + + "FROM ${Genre.TABLE} " + + "LEFT JOIN ${GenreSongMapping.TABLE} ON ${GenreSongMapping.TABLE}.${GenreSongMapping.COLUMN_GENRE_ID} = ${Genre.TABLE}.${Genre.COLUMN_ID} " + + "LEFT JOIN ${GenreSongMapping.TABLE} ON ${GenreSongMapping.TABLE}.${GenreSongMapping.COLUMN_GENRE_ID} = ${Genre.TABLE}.${Genre.COLUMN_ID} " + + "ORDER BY $orderBy $orderDirection" + return valuesAsFlow(SimpleSQLiteQuery(query)) + } + + @RawQuery + protected abstract fun entriesByNameNameIdMapped(query: SupportSQLiteQuery): Map< @MapColumn(Genre.COLUMN_NAME) String, @MapColumn(Genre.COLUMN_ID) String> -} - -fun GenreStore.findByIdAsFlow(id: String): Flow { - val query = "SELECT ${Genre.TABLE}.*, " + - "COUNT(${GenreSongMapping.TABLE}.${GenreSongMapping.COLUMN_SONG_ID}) as ${Genre.AlongAttributes.EMBEDDED_TRACKS_COUNT} " + - "FROM ${Genre.TABLE} " + - "LEFT JOIN ${GenreSongMapping.TABLE} ON ${GenreSongMapping.TABLE}.${GenreSongMapping.COLUMN_GENRE_ID} = ${Genre.TABLE}.${Genre.COLUMN_ID} " + - "LEFT JOIN ${GenreSongMapping.TABLE} ON ${GenreSongMapping.TABLE}.${GenreSongMapping.COLUMN_GENRE_ID} = ${Genre.TABLE}.${Genre.COLUMN_ID} " + - "WHERE ${Genre.COLUMN_ID} = ? " + - "LIMIT 1" - val args = arrayOf(id) - return findByIdAsFlowRaw(SimpleSQLiteQuery(query, args)) -} -fun GenreStore.valuesAsFlow( - sortBy: GenreRepository.SortBy, - sortReverse: Boolean, -): Flow> { - val orderBy = when (sortBy) { - GenreRepository.SortBy.CUSTOM -> "${Genre.TABLE}.${Genre.COLUMN_ID}" - GenreRepository.SortBy.GENRE -> "${Genre.TABLE}.${Genre.COLUMN_NAME}" - GenreRepository.SortBy.TRACKS_COUNT -> Genre.AlongAttributes.EMBEDDED_TRACKS_COUNT + fun entriesByNameNameIdMapped(names: Collection): Map< + @MapColumn(Genre.COLUMN_NAME) String, + @MapColumn(Genre.COLUMN_ID) String> { + val query = "SELECT ${Genre.COLUMN_ID}, ${Genre.COLUMN_NAME} " + + "FROM ${Genre.TABLE} " + + "WHERE ${Genre.COLUMN_NAME} in (${sqlqph(names.size)})" + val args = arrayOf(*names.toTypedArray()) + return entriesByNameNameIdMapped(SimpleSQLiteQuery(query, args)) } - val orderDirection = if (sortReverse) "DESC" else "ASC" - val query = "SELECT ${Genre.TABLE}.*, " + - "COUNT(${GenreSongMapping.TABLE}.${GenreSongMapping.COLUMN_SONG_ID}) as ${Genre.AlongAttributes.EMBEDDED_TRACKS_COUNT} " + - "FROM ${Genre.TABLE} " + - "LEFT JOIN ${GenreSongMapping.TABLE} ON ${GenreSongMapping.TABLE}.${GenreSongMapping.COLUMN_GENRE_ID} = ${Genre.TABLE}.${Genre.COLUMN_ID} " + - "LEFT JOIN ${GenreSongMapping.TABLE} ON ${GenreSongMapping.TABLE}.${GenreSongMapping.COLUMN_GENRE_ID} = ${Genre.TABLE}.${Genre.COLUMN_ID} " + - "ORDER BY $orderBy $orderDirection" - return valuesAsFlowRaw(SimpleSQLiteQuery(query)) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeFolderStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeFolderStore.kt index 75941376..71f58136 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeFolderStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeFolderStore.kt @@ -3,7 +3,6 @@ package io.github.zyrouge.symphony.services.database.store import androidx.room.Dao import androidx.room.Insert import androidx.room.MapColumn -import androidx.room.Query import androidx.room.RawQuery import androidx.room.Update import androidx.sqlite.db.SimpleSQLiteQuery @@ -14,44 +13,79 @@ import io.github.zyrouge.symphony.services.groove.repositories.MediaTreeReposito import kotlinx.coroutines.flow.Flow @Dao -interface MediaTreeFolderStore { +abstract class MediaTreeFolderStore { @Insert - suspend fun insert(vararg entities: MediaTreeFolder): List + abstract suspend fun insert(vararg entities: MediaTreeFolder): List @Update - suspend fun update(vararg entities: MediaTreeFolder): Int + abstract suspend fun update(vararg entities: MediaTreeFolder): Int - @Query("SELECT id FROM ${MediaTreeFolder.TABLE} WHERE ${MediaTreeFolder.COLUMN_PARENT_ID} = :parentId") - fun ids(parentId: String): List + @RawQuery + protected abstract fun ids(query: SimpleSQLiteQuery): List - @Query("SELECT * FROM ${MediaTreeFolder.TABLE} WHERE ${MediaTreeFolder.COLUMN_IS_HEAD} = 1 AND ${MediaTreeFolder.COLUMN_NAME} = :name") - fun findHeadByName(name: String): MediaTreeFolder? + fun ids(parentId: String): List { + val query = "SELECT ${MediaTreeFolder.TABLE}.${MediaTreeFolder.COLUMN_ID} " + + "FROM ${MediaTreeFolder.TABLE} " + + "WHERE ${MediaTreeFolder.COLUMN_PARENT_ID} = ?" + val args = arrayOf(parentId) + return ids(SimpleSQLiteQuery(query, args)) + } + + @RawQuery + protected abstract fun findHeadByName(query: SimpleSQLiteQuery): MediaTreeFolder? - @Query("SELECT * FROM ${MediaTreeFolder.TABLE} WHERE ${MediaTreeFolder.COLUMN_PARENT_ID} = :parentId AND ${MediaTreeFolder.COLUMN_NAME} = :name") - fun findByName(parentId: String, name: String): MediaTreeFolder? + fun findHeadByName(name: String): MediaTreeFolder? { + val query = "SELECT ${MediaTreeFolder.TABLE}.* " + + "FROM ${MediaTreeFolder.TABLE} " + + "WHERE ${MediaTreeFolder.COLUMN_IS_HEAD} = 1 " + + "AND ${MediaTreeFolder.COLUMN_NAME} = ?" + val args = arrayOf(name) + return findHeadByName(SimpleSQLiteQuery(query, args)) + } - @Query("SELECT * FROM ${MediaTreeFolder.TABLE} WHERE ${MediaTreeFolder.COLUMN_PARENT_ID} = :parentId") - fun entriesNameMapped(parentId: String): Map<@MapColumn(MediaTreeFolder.COLUMN_NAME) String, MediaTreeFolder> + @RawQuery + protected abstract fun findByName(query: SimpleSQLiteQuery): MediaTreeFolder? + + fun findByName(parentId: String, name: String): MediaTreeFolder? { + val query = "SELECT ${MediaTreeFolder.TABLE}.* " + + "FROM ${MediaTreeFolder.TABLE} " + + "WHERE ${MediaTreeFolder.COLUMN_PARENT_ID} = ? " + + "AND ${MediaTreeFolder.COLUMN_NAME} = ?" + val args = arrayOf(parentId, name) + return findByName(SimpleSQLiteQuery(query, args)) + } + + @RawQuery + protected abstract fun entriesNameMapped(query: SimpleSQLiteQuery): Map< + @MapColumn(MediaTreeFolder.COLUMN_NAME) String, MediaTreeFolder> + + fun entriesNameMapped(parentId: String): Map<@MapColumn(MediaTreeFolder.COLUMN_NAME) String, MediaTreeFolder> { + val query = "SELECT ${MediaTreeFolder.TABLE}.* " + + "FROM ${MediaTreeFolder.TABLE} " + + "WHERE ${MediaTreeFolder.COLUMN_PARENT_ID} = ?" + val args = arrayOf(parentId) + return entriesNameMapped(SimpleSQLiteQuery(query, args)) + } @RawQuery(observedEntities = [MediaTreeFolder::class, MediaTreeSongFile::class]) - fun valuesAsFlowRaw(query: SupportSQLiteQuery): Flow> -} + protected abstract fun valuesAsFlow(query: SupportSQLiteQuery): Flow> -fun MediaTreeFolderStore.valuesAsFlow( - sortBy: MediaTreeRepository.SortBy, - sortReverse: Boolean, -): Flow> { - val orderBy = when (sortBy) { - MediaTreeRepository.SortBy.CUSTOM -> "${MediaTreeFolder.TABLE}.${MediaTreeFolder.COLUMN_ID}" - MediaTreeRepository.SortBy.TITLE -> "${MediaTreeFolder.TABLE}.${MediaTreeFolder.COLUMN_NAME}" - MediaTreeRepository.SortBy.TRACKS_COUNT -> MediaTreeFolder.AlongAttributes.EMBEDDED_TRACKS_COUNT + fun valuesAsFlow( + sortBy: MediaTreeRepository.SortBy, + sortReverse: Boolean, + ): Flow> { + val orderBy = when (sortBy) { + MediaTreeRepository.SortBy.CUSTOM -> "${MediaTreeFolder.TABLE}.${MediaTreeFolder.COLUMN_ID}" + MediaTreeRepository.SortBy.TITLE -> "${MediaTreeFolder.TABLE}.${MediaTreeFolder.COLUMN_NAME}" + MediaTreeRepository.SortBy.TRACKS_COUNT -> MediaTreeFolder.AlongAttributes.EMBEDDED_TRACKS_COUNT + } + val orderDirection = if (sortReverse) "DESC" else "ASC" + val query = "SELECT ${MediaTreeFolder.TABLE}.*, " + + "COUNT(${MediaTreeSongFile.TABLE}.${MediaTreeSongFile.COLUMN_ID}) as ${MediaTreeFolder.AlongAttributes.EMBEDDED_TRACKS_COUNT} " + + "FROM ${MediaTreeFolder.TABLE} " + + "LEFT JOIN ${MediaTreeSongFile.TABLE} ON ${MediaTreeSongFile.TABLE}.${MediaTreeSongFile.COLUMN_PARENT_ID} = ${MediaTreeFolder.TABLE}.${MediaTreeFolder.COLUMN_ID} " + + "WHERE ${MediaTreeFolder.AlongAttributes.EMBEDDED_TRACKS_COUNT} > 0 " + + "ORDER BY $orderBy $orderDirection" + return valuesAsFlow(SimpleSQLiteQuery(query)) } - val orderDirection = if (sortReverse) "DESC" else "ASC" - val query = "SELECT ${MediaTreeFolder.TABLE}.*, " + - "COUNT(${MediaTreeSongFile.TABLE}.${MediaTreeSongFile.COLUMN_ID}) as ${MediaTreeFolder.AlongAttributes.EMBEDDED_TRACKS_COUNT} " + - "FROM ${MediaTreeFolder.TABLE} " + - "LEFT JOIN ${MediaTreeSongFile.TABLE} ON ${MediaTreeSongFile.TABLE}.${MediaTreeSongFile.COLUMN_PARENT_ID} = ${MediaTreeFolder.TABLE}.${MediaTreeFolder.COLUMN_ID} " + - "WHERE ${MediaTreeFolder.AlongAttributes.EMBEDDED_TRACKS_COUNT} > 0 " + - "ORDER BY $orderBy $orderDirection" - return valuesAsFlowRaw(SimpleSQLiteQuery(query)) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeLyricFileStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeLyricFileStore.kt index 7fda2db4..abd3209a 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeLyricFileStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeLyricFileStore.kt @@ -3,24 +3,50 @@ package io.github.zyrouge.symphony.services.database.store import androidx.room.Dao import androidx.room.Insert import androidx.room.MapColumn -import androidx.room.Query +import androidx.room.RawQuery import androidx.room.Update +import androidx.sqlite.db.SimpleSQLiteQuery import io.github.zyrouge.symphony.services.groove.entities.MediaTreeLyricFile @Dao -interface MediaTreeLyricFileStore { +abstract class MediaTreeLyricFileStore { @Insert - suspend fun insert(vararg entities: MediaTreeLyricFile): List + abstract suspend fun insert(vararg entities: MediaTreeLyricFile): List @Update - suspend fun update(vararg entities: MediaTreeLyricFile): Int + abstract suspend fun update(vararg entities: MediaTreeLyricFile): Int - @Query("SELECT id FROM ${MediaTreeLyricFile.TABLE} WHERE ${MediaTreeLyricFile.COLUMN_PARENT_ID} = :parentId") - fun ids(parentId: String): List + @RawQuery + protected abstract fun ids(query: SimpleSQLiteQuery): List - @Query("SELECT * FROM ${MediaTreeLyricFile.TABLE} WHERE ${MediaTreeLyricFile.COLUMN_PARENT_ID} = :parentId AND ${MediaTreeLyricFile.COLUMN_NAME} = :name LIMIT 1") - fun findByName(parentId: String, name: String): MediaTreeLyricFile? + fun ids(parentId: String): List { + val query = "SELECT ${MediaTreeLyricFile.COLUMN_ID} " + + "FROM ${MediaTreeLyricFile.TABLE} " + + "WHERE ${MediaTreeLyricFile.COLUMN_PARENT_ID} = ?" + val args = arrayOf(parentId) + return ids(SimpleSQLiteQuery(query, args)) + } - @Query("SELECT * FROM ${MediaTreeLyricFile.TABLE} WHERE ${MediaTreeLyricFile.COLUMN_PARENT_ID} = :parentId") - fun entriesNameMapped(parentId: String?): Map<@MapColumn(MediaTreeLyricFile.COLUMN_NAME) String, MediaTreeLyricFile> + @RawQuery + protected abstract fun findByName(query: SimpleSQLiteQuery): MediaTreeLyricFile? + + fun findByName(parentId: String, name: String): MediaTreeLyricFile? { + val query = "SELECT * FROM ${MediaTreeLyricFile.TABLE} " + + "WHERE ${MediaTreeLyricFile.COLUMN_PARENT_ID} = ? " + + "AND ${MediaTreeLyricFile.COLUMN_NAME} = ? " + + "LIMIT 1" + val args = arrayOf(parentId, name) + return findByName(SimpleSQLiteQuery(query, args)) + } + + @RawQuery + protected abstract fun entriesNameMapped(query: SimpleSQLiteQuery): Map< + @MapColumn(MediaTreeLyricFile.COLUMN_NAME) String, MediaTreeLyricFile> + + fun entriesNameMapped(parentId: String?): Map<@MapColumn(MediaTreeLyricFile.COLUMN_NAME) String, MediaTreeLyricFile> { + val query = "SELECT * FROM ${MediaTreeLyricFile.TABLE} " + + "WHERE ${MediaTreeLyricFile.COLUMN_PARENT_ID} = ?" + val args = arrayOf(parentId) + return entriesNameMapped(SimpleSQLiteQuery(query, args)) + } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeSongFileStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeSongFileStore.kt index 93e625dd..8d87c05e 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeSongFileStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeSongFileStore.kt @@ -3,27 +3,59 @@ package io.github.zyrouge.symphony.services.database.store import androidx.room.Dao import androidx.room.Insert import androidx.room.MapColumn -import androidx.room.Query +import androidx.room.RawQuery import androidx.room.Update +import androidx.sqlite.db.SimpleSQLiteQuery import io.github.zyrouge.symphony.services.groove.entities.MediaTreeSongFile +import io.github.zyrouge.symphony.utils.builtin.sqlqph @Dao -interface MediaTreeSongFileStore { +abstract class MediaTreeSongFileStore { @Insert - suspend fun insert(vararg entities: MediaTreeSongFile): List + abstract suspend fun insert(vararg entities: MediaTreeSongFile): List @Update - suspend fun update(vararg entities: MediaTreeSongFile): Int + abstract suspend fun update(vararg entities: MediaTreeSongFile): Int - @Query("DELETE FROM ${MediaTreeSongFile.TABLE} WHERE ${MediaTreeSongFile.COLUMN_ID} IN (:ids)") - suspend fun delete(ids: Collection): Int + @RawQuery + protected abstract suspend fun delete(query: SimpleSQLiteQuery): Int - @Query("SELECT id FROM ${MediaTreeSongFile.TABLE} WHERE ${MediaTreeSongFile.COLUMN_PARENT_ID} = :parentId") - fun ids(parentId: String): List + suspend fun delete(vararg ids: String): Int { + val query = "DELETE FROM ${MediaTreeSongFile.TABLE} " + + "WHERE ${MediaTreeSongFile.COLUMN_ID} IN (${sqlqph(ids.size)})" + return delete(SimpleSQLiteQuery(query, ids)) + } - @Query("SELECT * FROM ${MediaTreeSongFile.TABLE} WHERE $${MediaTreeSongFile.COLUMN_PARENT_ID} = :parentId AND ${MediaTreeSongFile.COLUMN_NAME} = :name LIMIT 1") - fun findByName(parentId: String, name: String): MediaTreeSongFile? + @RawQuery + protected abstract fun ids(query: SimpleSQLiteQuery): List - @Query("SELECT * FROM ${MediaTreeSongFile.TABLE} WHERE ${MediaTreeSongFile.COLUMN_PARENT_ID} = :parentId") - fun entriesNameMapped(parentId: String?): Map<@MapColumn(MediaTreeSongFile.COLUMN_NAME) String, MediaTreeSongFile> + fun ids(parentId: String): List { + val query = + "SELECT id FROM ${MediaTreeSongFile.TABLE} WHERE ${MediaTreeSongFile.COLUMN_PARENT_ID} = :parentId" + val args = arrayOf(parentId) + return ids(SimpleSQLiteQuery(query, args)) + } + + @RawQuery + protected abstract fun findByName(query: SimpleSQLiteQuery): MediaTreeSongFile? + + fun findByName(parentId: String, name: String): MediaTreeSongFile? { + val query = "SELECT * FROM ${MediaTreeSongFile.TABLE} " + + "WHERE ${MediaTreeSongFile.COLUMN_PARENT_ID} = ? " + + "AND ${MediaTreeSongFile.COLUMN_NAME} = ? " + + "LIMIT 1" + val args = arrayOf(parentId, name) + return findByName(SimpleSQLiteQuery(query, args)) + } + + @RawQuery + protected abstract fun entriesNameMapped(query: SimpleSQLiteQuery): Map< + @MapColumn(MediaTreeSongFile.COLUMN_NAME) String, MediaTreeSongFile> + + fun entriesNameMapped(parentId: String?): Map<@MapColumn(MediaTreeSongFile.COLUMN_NAME) String, MediaTreeSongFile> { + val query = "SELECT * FROM ${MediaTreeSongFile.TABLE} " + + "WHERE ${MediaTreeSongFile.COLUMN_PARENT_ID} = ?" + val args = arrayOf(parentId) + return entriesNameMapped(SimpleSQLiteQuery(query, args)) + } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistSongMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistSongMappingStore.kt index 9e6aa543..d5c36df2 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistSongMappingStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistSongMappingStore.kt @@ -2,7 +2,6 @@ package io.github.zyrouge.symphony.services.database.store import androidx.room.Dao import androidx.room.Insert -import androidx.room.Query import androidx.room.RawQuery import androidx.sqlite.db.SimpleSQLiteQuery import io.github.zyrouge.symphony.services.groove.entities.Playlist @@ -10,73 +9,83 @@ import io.github.zyrouge.symphony.services.groove.entities.PlaylistSongMapping import io.github.zyrouge.symphony.services.groove.entities.Song import io.github.zyrouge.symphony.services.groove.entities.SongArtworkIndex import io.github.zyrouge.symphony.services.groove.repositories.SongRepository +import io.github.zyrouge.symphony.utils.builtin.sqlqph import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.mapLatest @Dao -interface PlaylistSongMappingStore { +abstract class PlaylistSongMappingStore { @Insert - suspend fun insert(vararg entities: PlaylistSongMapping) + abstract suspend fun insert(vararg entities: PlaylistSongMapping) - @Query("DELETE FROM ${PlaylistSongMapping.TABLE} WHERE ${PlaylistSongMapping.COLUMN_PLAYLIST_ID} IN (:ids)") - suspend fun deletePlaylistIds(ids: Collection) + @RawQuery + abstract suspend fun deletePlaylistIds(query: SimpleSQLiteQuery): Int + + suspend fun deletePlaylistIds(vararg ids: String): Int { + val query = "DELETE FROM ${PlaylistSongMapping.TABLE} " + + "WHERE ${PlaylistSongMapping.COLUMN_PLAYLIST_ID} IN (${sqlqph(ids.size)})" + return deletePlaylistIds(SimpleSQLiteQuery(query, ids)) + } @RawQuery(observedEntities = [SongArtworkIndex::class, PlaylistSongMapping::class]) - fun findTop4SongArtworksAsFlowRaw(query: SimpleSQLiteQuery): Flow> + protected abstract fun findTop4SongArtworksAsFlowRaw(query: SimpleSQLiteQuery): Flow> - @RawQuery - fun findSongIdsByPlaylistInternalIdAsFlowRaw(query: SimpleSQLiteQuery): Flow> -} + fun findTop4SongArtworksAsFlow(playlistId: String): Flow> { + val query = "SELECT ${SongArtworkIndex.TABLE}.* " + + "FROM ${SongArtworkIndex.TABLE} " + + "LEFT JOIN ${PlaylistSongMapping.TABLE} ON ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_PLAYLIST_ID} = ?" + + "AND ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_SONG_ID} = ${SongArtworkIndex.TABLE}.${SongArtworkIndex.COLUMN_SONG_ID} " + + "WHERE ${SongArtworkIndex.TABLE}.${SongArtworkIndex.COLUMN_FILE} != null " + + "ORDER BY DESC " + + "LIMIT 4" + val args = arrayOf(playlistId) + return findTop4SongArtworksAsFlowRaw(SimpleSQLiteQuery(query, args)) + } -@OptIn(ExperimentalCoroutinesApi::class) -fun PlaylistSongMappingStore.valuesMappedAsFlow( - songStore: SongStore, - id: String, - sortBy: SongRepository.SortBy, - sortReverse: Boolean, -): Flow> { - val query = songStore.valuesQuery( - sortBy, - sortReverse, - additionalClauseBeforeJoins = "JOIN ${PlaylistSongMapping.TABLE} ON ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_PLAYLIST_ID} = ? AND ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_SONG_ID} = ${Song.COLUMN_ID} ", - additionalArgsBeforeJoins = arrayOf(id), - ) - val entries = songStore.entriesAsPlaylistSongMappedAsFlowRaw(query) - return entries.mapLatest { transformEntriesAsValues(it) } -} + @RawQuery + protected abstract fun findSongIdsByPlaylistInternalIdAsFlowRaw(query: SimpleSQLiteQuery): Flow> -fun PlaylistSongMappingStore.transformEntriesAsValues(entries: Map): List { - val list = mutableListOf() - var head = entries.firstNotNullOfOrNull { - when { - it.value.mapping.isHead -> it.value - else -> null - } - } - while (head != null) { - list.add(head.song) - head = entries[head.mapping.nextId] + @OptIn(ExperimentalCoroutinesApi::class) + fun findSongIdsByPlaylistInternalIdAsFlow(playlistInternalId: Int): Flow> { + val query = "SELECT ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_SONG_ID} " + + "FROM ${PlaylistSongMapping.TABLE} " + + "LEFT JOIN ${Playlist.TABLE} ON ${Playlist.TABLE}.${Playlist.COLUMN_INTERNAL_ID} = ?" + + "AND ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_PLAYLIST_ID} = ${Playlist.TABLE}.${Playlist.COLUMN_ID} " + val args = arrayOf(playlistInternalId) + return findSongIdsByPlaylistInternalIdAsFlowRaw(SimpleSQLiteQuery(query, args)) } - return list.toList() -} -fun PlaylistSongMappingStore.findTop4SongArtworksAsFlow(playlistId: String): Flow> { - val query = "SELECT ${SongArtworkIndex.TABLE}.* " + - "FROM ${SongArtworkIndex.TABLE} " + - "LEFT JOIN ${PlaylistSongMapping.TABLE} ON ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_PLAYLIST_ID} = ? AND ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_SONG_ID} = ${SongArtworkIndex.TABLE}.${SongArtworkIndex.COLUMN_SONG_ID} " + - "WHERE ${SongArtworkIndex.TABLE}.${SongArtworkIndex.COLUMN_FILE} != null " + - "ORDER BY DESC " + - "LIMIT 4" - val args = arrayOf(playlistId) - return findTop4SongArtworksAsFlowRaw(SimpleSQLiteQuery(query, args)) -} + @OptIn(ExperimentalCoroutinesApi::class) + fun valuesMappedAsFlow( + songStore: SongStore, + id: String, + sortBy: SongRepository.SortBy, + sortReverse: Boolean, + ): Flow> { + val query = songStore.valuesQuery( + sortBy, + sortReverse, + additionalClauseBeforeJoins = "JOIN ${PlaylistSongMapping.TABLE} ON ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_PLAYLIST_ID} = ?" + + "AND ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_SONG_ID} = ${Song.COLUMN_ID} ", + additionalArgsBeforeJoins = arrayOf(id), + ) + val entries = songStore.entriesAsPlaylistSongMappedAsFlowRaw(query) + return entries.mapLatest { transformEntriesAsValues(it) } + } -@OptIn(ExperimentalCoroutinesApi::class) -fun PlaylistSongMappingStore.findSongIdsByPlaylistInternalIdAsFlow(playlistInternalId: Int): Flow> { - val query = "SELECT ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_SONG_ID} " + - "FROM ${PlaylistSongMapping.TABLE} " + - "LEFT JOIN ${Playlist.TABLE} ON ${Playlist.TABLE}.${Playlist.COLUMN_INTERNAL_ID} = ? AND ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_PLAYLIST_ID} = ${Playlist.TABLE}.${Playlist.COLUMN_ID} " - val args = arrayOf(playlistInternalId) - return findSongIdsByPlaylistInternalIdAsFlowRaw(SimpleSQLiteQuery(query, args)) + fun transformEntriesAsValues(entries: Map): List { + val list = mutableListOf() + var head = entries.firstNotNullOfOrNull { + when { + it.value.mapping.isHead -> it.value + else -> null + } + } + while (head != null) { + list.add(head.song) + head = entries[head.mapping.nextId] + } + return list.toList() + } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistStore.kt index 52e17561..c9aadb9f 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistStore.kt @@ -13,51 +13,56 @@ import io.github.zyrouge.symphony.services.groove.repositories.PlaylistRepositor import kotlinx.coroutines.flow.Flow @Dao -interface PlaylistStore { +abstract class PlaylistStore { @Insert - suspend fun insert(vararg entities: Playlist): List + abstract suspend fun insert(vararg entities: Playlist): List @Update - suspend fun update(vararg entities: Playlist): Int + abstract suspend fun update(vararg entities: Playlist): Int @Query("DELETE FROM ${Playlist.TABLE} WHERE ${Playlist.COLUMN_ID} = :id") - suspend fun delete(id: String): Int + abstract suspend fun delete(id: String): Int @RawQuery(observedEntities = [Playlist::class, PlaylistSongMapping::class]) - fun findByIdAsFlowRaw(query: SupportSQLiteQuery): Flow + protected abstract fun findByIdAsFlow(query: SupportSQLiteQuery): Flow - @Query("SELECT * FROM ${Playlist.TABLE} WHERE ${Playlist.COLUMN_URI} != NULL") - fun valuesLocalOnly(): List + fun findByIdAsFlow(id: String): Flow { + val query = "SELECT ${Playlist.TABLE}.*, " + + "COUNT(${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_SONG_ID}) as ${Playlist.AlongAttributes.EMBEDDED_TRACKS_COUNT} " + + "FROM ${Playlist.TABLE} " + + "LEFT JOIN ${PlaylistSongMapping.TABLE} ON ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_PLAYLIST_ID} = ${Playlist.TABLE}.${Playlist.COLUMN_ID} " + + "WHERE ${Playlist.TABLE}.${Playlist.COLUMN_ID} = ? " + + "LIMIT 1" + val args = arrayOf(id) + return findByIdAsFlow(SimpleSQLiteQuery(query, args)) + } - @RawQuery(observedEntities = [Playlist::class, PlaylistSongMapping::class]) - fun valuesAsFlowRaw(query: SupportSQLiteQuery): Flow> -} + @RawQuery + protected abstract fun valuesLocalOnly(query: SimpleSQLiteQuery): List -fun PlaylistStore.findByIdAsFlow(id: String): Flow { - val query = "SELECT ${Playlist.TABLE}.*, " + - "COUNT(${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_SONG_ID}) as ${Playlist.AlongAttributes.EMBEDDED_TRACKS_COUNT} " + - "FROM ${Playlist.TABLE} " + - "LEFT JOIN ${PlaylistSongMapping.TABLE} ON ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_PLAYLIST_ID} = ${Playlist.TABLE}.${Playlist.COLUMN_ID} " + - "WHERE ${Playlist.TABLE}.${Playlist.COLUMN_ID} = ? " + - "LIMIT 1" - val args = arrayOf(id) - return findByIdAsFlowRaw(SimpleSQLiteQuery(query, args)) -} + fun valuesLocalOnly(): List { + val query = "SELECT * FROM ${Playlist.TABLE} WHERE ${Playlist.COLUMN_URI} != NULL" + return valuesLocalOnly(SimpleSQLiteQuery(query)) + } + + @RawQuery(observedEntities = [Playlist::class, PlaylistSongMapping::class]) + protected abstract fun valuesAsFlow(query: SupportSQLiteQuery): Flow> -fun PlaylistStore.valuesAsFlow( - sortBy: PlaylistRepository.SortBy, - sortReverse: Boolean, -): Flow> { - val orderBy = when (sortBy) { - PlaylistRepository.SortBy.CUSTOM -> "${Playlist.TABLE}.${Playlist.COLUMN_ID}" - PlaylistRepository.SortBy.TITLE -> "${Playlist.TABLE}.${Playlist.COLUMN_TITLE}" - PlaylistRepository.SortBy.TRACKS_COUNT -> Playlist.AlongAttributes.EMBEDDED_TRACKS_COUNT + fun valuesAsFlow( + sortBy: PlaylistRepository.SortBy, + sortReverse: Boolean, + ): Flow> { + val orderBy = when (sortBy) { + PlaylistRepository.SortBy.CUSTOM -> "${Playlist.TABLE}.${Playlist.COLUMN_ID}" + PlaylistRepository.SortBy.TITLE -> "${Playlist.TABLE}.${Playlist.COLUMN_TITLE}" + PlaylistRepository.SortBy.TRACKS_COUNT -> Playlist.AlongAttributes.EMBEDDED_TRACKS_COUNT + } + val orderDirection = if (sortReverse) "DESC" else "ASC" + val query = "SELECT ${Playlist.TABLE}.*, " + + "COUNT(${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_SONG_ID}) as ${Playlist.AlongAttributes.EMBEDDED_TRACKS_COUNT} " + + "FROM ${Playlist.TABLE} " + + "LEFT JOIN ${PlaylistSongMapping.TABLE} ON ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_PLAYLIST_ID} = ${Playlist.TABLE}.${Playlist.COLUMN_ID} " + + "ORDER BY $orderBy $orderDirection" + return valuesAsFlow(SimpleSQLiteQuery(query)) } - val orderDirection = if (sortReverse) "DESC" else "ASC" - val query = "SELECT ${Playlist.TABLE}.*, " + - "COUNT(${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_SONG_ID}) as ${Playlist.AlongAttributes.EMBEDDED_TRACKS_COUNT} " + - "FROM ${Playlist.TABLE} " + - "LEFT JOIN ${PlaylistSongMapping.TABLE} ON ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_PLAYLIST_ID} = ${Playlist.TABLE}.${Playlist.COLUMN_ID} " + - "ORDER BY $orderBy $orderDirection" - return valuesAsFlowRaw(SimpleSQLiteQuery(query)) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongArtworkIndexStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongArtworkIndexStore.kt index 22ca3da3..16892221 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongArtworkIndexStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongArtworkIndexStore.kt @@ -4,18 +4,34 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.MapColumn import androidx.room.OnConflictStrategy -import androidx.room.Query +import androidx.room.RawQuery +import androidx.sqlite.db.SimpleSQLiteQuery import io.github.zyrouge.symphony.services.groove.entities.SongArtworkIndex import kotlinx.coroutines.flow.Flow @Dao -interface SongArtworkIndexStore { +abstract class SongArtworkIndexStore { @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun upsert(vararg entities: SongArtworkIndex): List + abstract suspend fun upsert(vararg entities: SongArtworkIndex): List - @Query("SELECT * FROM ${SongArtworkIndex.TABLE} WHERE ${SongArtworkIndex.COLUMN_SONG_ID} = :songId LIMIT 1") - fun findBySongIdAsFlow(songId: String): Flow + @RawQuery + protected abstract fun findBySongIdAsFlow(query: SimpleSQLiteQuery): Flow - @Query("SELECT * FROM ${SongArtworkIndex.TABLE} WHERE ${SongArtworkIndex.COLUMN_SONG_ID} != null") - fun entriesSongIdMapped(): Map<@MapColumn(SongArtworkIndex.COLUMN_SONG_ID) String, SongArtworkIndex> + fun findBySongIdAsFlow(songId: String): Flow { + val query = "SELECT * FROM ${SongArtworkIndex.TABLE} " + + "WHERE ${SongArtworkIndex.COLUMN_SONG_ID} = ? " + + "LIMIT 1" + val args = arrayOf(songId) + return findBySongIdAsFlow(SimpleSQLiteQuery(query, args)) + } + + @RawQuery + protected abstract fun entriesSongIdMapped(query: SimpleSQLiteQuery): Map< + @MapColumn(SongArtworkIndex.COLUMN_SONG_ID) String, SongArtworkIndex> + + fun entriesSongIdMapped(): Map<@MapColumn(SongArtworkIndex.COLUMN_SONG_ID) String, SongArtworkIndex> { + val query = "SELECT * FROM ${SongArtworkIndex.TABLE} " + + "WHERE ${SongArtworkIndex.COLUMN_SONG_ID} != null" + return entriesSongIdMapped(SimpleSQLiteQuery(query)) + } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongLyricStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongLyricStore.kt index 52782769..8f289dde 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongLyricStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongLyricStore.kt @@ -6,7 +6,7 @@ import androidx.room.OnConflictStrategy import io.github.zyrouge.symphony.services.groove.entities.SongLyric @Dao -interface SongLyricStore { +abstract class SongLyricStore { @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun upsert(vararg entities: SongLyric) + abstract suspend fun upsert(vararg entities: SongLyric) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueSongMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueSongMappingStore.kt index d3570912..3034ce29 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueSongMappingStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueSongMappingStore.kt @@ -3,58 +3,110 @@ package io.github.zyrouge.symphony.services.database.store import androidx.room.Dao import androidx.room.Insert import androidx.room.MapColumn -import androidx.room.Query import androidx.room.RawQuery +import androidx.room.Update import androidx.sqlite.db.SimpleSQLiteQuery import androidx.sqlite.db.SupportSQLiteQuery import io.github.zyrouge.symphony.services.groove.entities.Song import io.github.zyrouge.symphony.services.groove.entities.SongQueueSongMapping +import io.github.zyrouge.symphony.utils.builtin.sqlqph import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.mapLatest @Dao -interface SongQueueSongMappingStore { +abstract class SongQueueSongMappingStore { @Insert - suspend fun insert(vararg entities: SongQueueSongMapping) + abstract suspend fun insert(vararg entities: SongQueueSongMapping) - @Query("DELETE FROM ${SongQueueSongMapping.TABLE} WHERE ${SongQueueSongMapping.COLUMN_QUEUE_ID} IN (:ids)") - suspend fun deleteSongQueueIds(ids: Collection) + @Update + abstract suspend fun update(vararg entities: SongQueueSongMapping) + + @RawQuery + protected abstract suspend fun delete(query: SimpleSQLiteQuery): Int + + suspend fun delete(queueId: String, ids: Collection): Int { + val query = "DELETE FROM ${SongQueueSongMapping.TABLE} " + + "WHERE ${SongQueueSongMapping.COLUMN_QUEUE_ID} = ? " + + "AND ${SongQueueSongMapping.COLUMN_ID} IN (${sqlqph(ids.size)})" + val args = arrayOf(queueId, *ids.toTypedArray()) + return delete(SimpleSQLiteQuery(query, args)) + } + + @RawQuery + protected abstract fun findById(query: SupportSQLiteQuery): Song.AlongSongQueueMapping? + + fun findById(queueId: String, id: String): Song.AlongSongQueueMapping? { + val query = "SELECT ${Song.TABLE}.*, " + + "${SongQueueSongMapping.TABLE}.* " + + "FROM ${SongQueueSongMapping.TABLE} " + + "WHERE ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_QUEUE_ID} = ? AND ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_ID} = ? " + + "LEFT JOIN ${Song.TABLE} ON ${Song.TABLE}.${Song.COLUMN_ID} = ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_SONG_ID} " + val args = arrayOf(queueId, id) + return findById(SimpleSQLiteQuery(query, args)) + } + + @RawQuery + protected abstract fun findByNextIdRaw(query: SupportSQLiteQuery): Song.AlongSongQueueMapping? + + fun findByNextId(queueId: String, nextId: String): Song.AlongSongQueueMapping? { + val query = "SELECT ${Song.TABLE}.*, " + + "${SongQueueSongMapping.TABLE}.* " + + "FROM ${SongQueueSongMapping.TABLE} " + + "WHERE ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_QUEUE_ID} = ? AND ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_NEXT_ID} = ? " + + "LEFT JOIN ${Song.TABLE} ON ${Song.TABLE}.${Song.COLUMN_ID} = ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_SONG_ID} " + val args = arrayOf(queueId, nextId) + return findByNextIdRaw(SimpleSQLiteQuery(query, args)) + } + + @RawQuery + protected abstract fun findHeadRaw(query: SupportSQLiteQuery): Song.AlongSongQueueMapping? + + fun findHead(queueId: String): Song.AlongSongQueueMapping? { + val query = "SELECT ${Song.TABLE}.*, " + + "${SongQueueSongMapping.TABLE}.* " + + "FROM ${SongQueueSongMapping.TABLE} " + + "WHERE ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_QUEUE_ID} = ? AND ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_IS_HEAD} = true " + + "LEFT JOIN ${Song.TABLE} ON ${Song.TABLE}.${Song.COLUMN_ID} = ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_SONG_ID} " + val args = arrayOf(queueId) + return findHeadRaw(SimpleSQLiteQuery(query, args)) + } @RawQuery(observedEntities = [Song::class, SongQueueSongMapping::class]) - fun entriesAsFlowRaw(query: SupportSQLiteQuery): Flow> -} + protected abstract fun entriesAsFlowRaw(query: SupportSQLiteQuery): Flow< + Map<@MapColumn(SongQueueSongMapping.COLUMN_SONG_ID) String, Song.AlongSongQueueMapping>> -fun SongQueueSongMappingStore.entriesAsFlow(queueId: String): Flow> { - val query = "SELECT ${Song.TABLE}.*, " + - "${SongQueueSongMapping.TABLE}.* " + - "FROM ${SongQueueSongMapping.TABLE} " + - "WHERE ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_QUEUE_ID} = ? " + - "LEFT JOIN ${Song.TABLE} ON ${Song.TABLE}.${Song.COLUMN_ID} = ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_SONG_ID} " + - "ORDER BY ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_IS_HEAD} DESC" - val args = arrayOf(queueId) - return entriesAsFlowRaw(SimpleSQLiteQuery(query, args)) -} + fun entriesAsFlow(queueId: String): Flow> { + val query = "SELECT ${Song.TABLE}.*, " + + "${SongQueueSongMapping.TABLE}.* " + + "FROM ${SongQueueSongMapping.TABLE} " + + "WHERE ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_QUEUE_ID} = ? " + + "LEFT JOIN ${Song.TABLE} ON ${Song.TABLE}.${Song.COLUMN_ID} = ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_SONG_ID} " + + "ORDER BY ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_IS_HEAD} DESC" + val args = arrayOf(queueId) + return entriesAsFlowRaw(SimpleSQLiteQuery(query, args)) + } -@OptIn(ExperimentalCoroutinesApi::class) -fun SongQueueSongMappingStore.transformEntriesAsValuesFlow(entries: Flow>): Flow> { - return entries.mapLatest { - val list = mutableListOf() - var head = it.firstNotNullOfOrNull { - when { - it.value.mapping.isHead -> it.value - else -> null + @OptIn(ExperimentalCoroutinesApi::class) + fun transformEntriesAsValuesFlow(entries: Flow>): Flow> { + return entries.mapLatest { + val list = mutableListOf() + var head = it.firstNotNullOfOrNull { + when { + it.value.mapping.isHead -> it.value + else -> null + } } + while (head != null) { + list.add(head.entity) + head = it[head.mapping.nextId] + } + list.toList() } - while (head != null) { - list.add(head.entity) - head = it[head.mapping.nextId] - } - list.toList() } -} -fun SongQueueSongMappingStore.valuesAsFlow(queueId: String): Flow> { - val entries = entriesAsFlow(queueId) - return transformEntriesAsValuesFlow(entries) + fun valuesAsFlow(queueId: String): Flow> { + val entries = entriesAsFlow(queueId) + return transformEntriesAsValuesFlow(entries) + } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueStore.kt index 26f149ed..c67bbe8d 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueStore.kt @@ -2,8 +2,8 @@ package io.github.zyrouge.symphony.services.database.store import androidx.room.Dao import androidx.room.Insert -import androidx.room.Query import androidx.room.RawQuery +import androidx.room.Update import androidx.sqlite.db.SimpleSQLiteQuery import androidx.sqlite.db.SupportSQLiteQuery import io.github.zyrouge.symphony.services.groove.entities.SongQueue @@ -11,21 +11,51 @@ import io.github.zyrouge.symphony.services.groove.entities.SongQueueSongMapping import kotlinx.coroutines.flow.Flow @Dao -interface SongQueueStore { +abstract class SongQueueStore { @Insert - suspend fun insert(vararg entities: SongQueue): List + abstract suspend fun insert(vararg entities: SongQueue): List - @Query("DELETE FROM ${SongQueue.TABLE} WHERE ${SongQueue.COLUMN_ID} = :id") - suspend fun delete(id: String): Int + @Update + abstract suspend fun update(vararg entities: SongQueue): Int + + @RawQuery + protected abstract suspend fun delete(query: SimpleSQLiteQuery): Int + + suspend fun delete(vararg ids: String): Int { + val query = "DELETE FROM ${SongQueue.TABLE} WHERE ${SongQueue.COLUMN_ID} = :id" + return delete(SimpleSQLiteQuery(query, ids)) + } + + @RawQuery + protected abstract fun findByInternalId(query: SimpleSQLiteQuery): SongQueue.AlongAttributes? + + fun findByInternalId(internalId: Int): SongQueue.AlongAttributes? { + val query = "SELECT * FROM ${SongQueue.TABLE} " + + "WHERE ${SongQueue.COLUMN_INTERNAL_ID} = ?" + val args = arrayOf(internalId) + return findByInternalId(SimpleSQLiteQuery(query, args)) + } @RawQuery(observedEntities = [SongQueue::class, SongQueueSongMapping::class]) - fun valuesAsFlowRaw(query: SupportSQLiteQuery): Flow> -} + protected abstract fun findFirstAsFlow(query: SupportSQLiteQuery): Flow + + fun findFirstAsFlow(): Flow { + val query = "SELECT ${SongQueue.TABLE}.*, " + + "COUNT(${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_SONG_ID}) as ${SongQueue.AlongAttributes.EMBEDDED_TRACKS_COUNT} " + + "FROM ${SongQueue.TABLE} " + + "LEFT JOIN ${SongQueueSongMapping.TABLE} ON ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_QUEUE_ID} = ${SongQueue.TABLE}.${SongQueue.COLUMN_ID} " + + "LIMIT 1" + return findFirstAsFlow(SimpleSQLiteQuery(query)) + } + +// @RawQuery(observedEntities = [SongQueue::class, SongQueueSongMapping::class]) +// fun valuesAsFlowRaw(query: SupportSQLiteQuery): Flow> -fun SongQueueStore.valuesAsFlow(): Flow> { - val query = "SELECT ${SongQueue.TABLE}.*, " + - "COUNT(${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_SONG_ID}) as ${SongQueue.AlongAttributes.EMBEDDED_TRACKS_COUNT} " + - "FROM ${SongQueue.TABLE} " + - "LEFT JOIN ${SongQueueSongMapping.TABLE} ON ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_QUEUE_ID} = ${SongQueue.TABLE}.${SongQueue.COLUMN_ID}" - return valuesAsFlowRaw(SimpleSQLiteQuery(query)) +//fun SongQueueStore.valuesAsFlow(): Flow> { +// val query = "SELECT ${SongQueue.TABLE}.*, " + +// "COUNT(${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_SONG_ID}) as ${SongQueue.AlongAttributes.EMBEDDED_TRACKS_COUNT} " + +// "FROM ${SongQueue.TABLE} " + +// "LEFT JOIN ${SongQueueSongMapping.TABLE} ON ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_QUEUE_ID} = ${SongQueue.TABLE}.${SongQueue.COLUMN_ID}" +// return valuesAsFlowRaw(SimpleSQLiteQuery(query)) +//} } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongStore.kt index e4117498..e905df67 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongStore.kt @@ -3,7 +3,6 @@ package io.github.zyrouge.symphony.services.database.store import androidx.room.Dao import androidx.room.Insert import androidx.room.MapColumn -import androidx.room.Query import androidx.room.RawQuery import androidx.room.Update import androidx.sqlite.db.SimpleSQLiteQuery @@ -19,27 +18,41 @@ import io.github.zyrouge.symphony.services.groove.entities.GenreSongMapping import io.github.zyrouge.symphony.services.groove.entities.PlaylistSongMapping import io.github.zyrouge.symphony.services.groove.entities.Song import io.github.zyrouge.symphony.services.groove.repositories.SongRepository +import io.github.zyrouge.symphony.utils.builtin.sqlqph import kotlinx.coroutines.flow.Flow @Dao -interface SongStore { +abstract class SongStore { @Insert - suspend fun insert(vararg entities: Song): List + abstract suspend fun insert(vararg entities: Song): List @Update - suspend fun update(vararg entities: Song): Int + abstract suspend fun update(vararg entities: Song): Int - @Query("DELETE FROM ${Song.TABLE} WHERE ${Song.COLUMN_ID} = :id") - suspend fun delete(id: String): Int + @RawQuery + protected abstract suspend fun delete(query: SimpleSQLiteQuery): Int + + suspend fun delete(vararg ids: String): Int { + val query = "DELETE FROM ${Song.TABLE} WHERE ${Song.COLUMN_ID} IN (${sqlqph(ids.size)})" + return delete(SimpleSQLiteQuery(query, ids)) + } + + @RawQuery + protected abstract fun findByPath(query: SimpleSQLiteQuery): Song? - @Query("DELETE FROM ${Song.TABLE} WHERE ${Song.COLUMN_ID} IN (:ids)") - suspend fun delete(ids: Collection): Int + fun findByPath(path: String): Song? { + val query = "SELECT * FROM ${Song.TABLE} WHERE ${Song.COLUMN_PATH} = ? LIMIT 1" + val args = arrayOf(path) + return findByPath(SimpleSQLiteQuery(query, args)) + } - @Query("SELECT * FROM ${Song.TABLE} WHERE ${Song.COLUMN_PATH} = :path LIMIT 1") - fun findByPath(path: String): Song? + @RawQuery + protected abstract fun ids(query: SimpleSQLiteQuery): List - @Query("SELECT ${Song.COLUMN_ID} FROM ${Song.TABLE}") - fun ids(): List + fun ids(): List { + val query = "SELECT ${Song.COLUMN_ID} FROM ${Song.TABLE}" + return ids(SimpleSQLiteQuery(query)) + } @RawQuery( observedEntities = [ @@ -51,7 +64,7 @@ interface SongStore { Song::class, ] ) - fun entriesAsFlowRaw(query: SupportSQLiteQuery): Flow> + protected abstract fun entriesAsFlow(query: SupportSQLiteQuery): Flow> @RawQuery( observedEntities = [ @@ -63,10 +76,90 @@ interface SongStore { Song::class, ] ) - fun entriesAsPlaylistSongMappedAsFlowRaw(query: SupportSQLiteQuery): Flow> + internal abstract fun entriesAsPlaylistSongMappedAsFlowRaw(query: SupportSQLiteQuery): Flow< + Map<@MapColumn(Song.COLUMN_ID) String, Song.AlongPlaylistMapping>> + + fun valuesQuery( + sortBy: SongRepository.SortBy, + sortReverse: Boolean, + additionalClauseAfterSongSelect: String = "", + additionalClauseBeforeJoins: String = "", + additionalArgsBeforeJoins: Array = emptyArray(), + overrideOrderBy: String? = null, + ): SupportSQLiteQuery { + val aliasFirstAlbumArtist = "firstAlbumArtist" + val embeddedFirstArtistName = "firstArtistName" + val embeddedFirstAlbumName = "firstAlbumName" + val embeddedFirstAlbumArtistName = "firstAlbumArtistName" + val embeddedFirstComposerName = "firstComposerName" + val orderBy = overrideOrderBy ?: when (sortBy) { + SongRepository.SortBy.CUSTOM -> "${Song.TABLE}.${Song.COLUMN_ID}" + SongRepository.SortBy.TITLE -> "${Song.TABLE}.${Song.COLUMN_ID}" + SongRepository.SortBy.ARTIST -> embeddedFirstArtistName + SongRepository.SortBy.ALBUM -> embeddedFirstAlbumName + SongRepository.SortBy.DURATION -> "${Song.TABLE}.${Song.COLUMN_ID}" + SongRepository.SortBy.DATE_MODIFIED -> "${Song.TABLE}.${Song.COLUMN_ID}" + SongRepository.SortBy.COMPOSER -> embeddedFirstComposerName + SongRepository.SortBy.ALBUM_ARTIST -> embeddedFirstAlbumArtistName + SongRepository.SortBy.YEAR -> "${Song.TABLE}.${Song.COLUMN_YEAR}" + SongRepository.SortBy.FILENAME -> "${Song.TABLE}.${Song.COLUMN_FILENAME}" + SongRepository.SortBy.TRACK_NUMBER -> "${Song.TABLE}.${Song.COLUMN_TRACK_NUMBER}" + } + val orderDirection = if (sortReverse) "DESC" else "ASC" + val artistQuery = "SELECT" + + "TOP 1 ${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_ARTIST_ID} " + + "FROM ${ArtistSongMapping.TABLE} " + + "WHERE ${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_SONG_ID} = ${Song.COLUMN_ID}" + val albumQuery = "SELECT" + + "TOP 1 ${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_ALBUM_ID} " + + "FROM ${AlbumSongMapping.TABLE} " + + "WHERE ${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_SONG_ID} = ${Song.COLUMN_ID}" + val albumArtistQuery = "SELECT " + + "TOP 1 ${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ARTIST_ID} " + + "FROM ${AlbumArtistMapping.TABLE} " + + "WHERE ${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID} = ${Album.COLUMN_ID}" + val composerQuery = "SELECT " + + "TOP 1 ${ComposerSongMapping.TABLE}.${ComposerSongMapping.COLUMN_COMPOSER_ID} " + + "FROM ${ComposerSongMapping.TABLE} " + + "WHERE ${ComposerSongMapping.TABLE}.${ComposerSongMapping.COLUMN_SONG_ID} = ${Song.COLUMN_ID}" + val query = "SELECT ${Song.TABLE}.*, " + + additionalClauseAfterSongSelect + + "${Artist.TABLE}.${Artist.COLUMN_NAME} as $embeddedFirstArtistName, " + + "${Album.TABLE}.${Album.COLUMN_NAME} as $embeddedFirstAlbumName, " + + "$aliasFirstAlbumArtist.${Artist.COLUMN_NAME} as $embeddedFirstAlbumArtistName, " + + "${Composer.TABLE}.${Composer.COLUMN_NAME} as $embeddedFirstComposerName " + + "FROM ${Song.TABLE} " + + additionalClauseBeforeJoins + + "LEFT JOIN ${Artist.TABLE} ON ${Artist.TABLE}.${Artist.COLUMN_ID} = ($artistQuery)" + + "LEFT JOIN ${Album.TABLE} ON ${Album.TABLE}.${Album.COLUMN_ID} = ($albumQuery)" + + "LEFT JOIN ${Artist.TABLE} $aliasFirstAlbumArtist ON ${Artist.TABLE}.${Artist.COLUMN_ID} = ($albumArtistQuery)" + + "LEFT JOIN ${Composer.TABLE} ON ${Composer.TABLE}.${Composer.COLUMN_ID} = ($composerQuery)" + + "ORDER BY $orderBy $orderDirection" + val args = additionalArgsBeforeJoins + return SimpleSQLiteQuery(query, args) + } @RawQuery - fun valuesRaw(query: SupportSQLiteQuery): List + protected abstract fun values(query: SupportSQLiteQuery): List + + fun values( + sortBy: SongRepository.SortBy, + sortReverse: Boolean, + additionalClauseAfterSongSelect: String = "", + additionalClauseBeforeJoins: String = "", + additionalArgsBeforeJoins: Array = emptyArray(), + overrideOrderBy: String? = null, + ): List { + val query = valuesQuery( + sortBy = sortBy, + sortReverse = sortReverse, + additionalClauseAfterSongSelect = additionalClauseAfterSongSelect, + additionalClauseBeforeJoins = additionalClauseBeforeJoins, + additionalArgsBeforeJoins = additionalArgsBeforeJoins, + overrideOrderBy = overrideOrderBy, + ) + return values(query) + } @RawQuery( observedEntities = [ @@ -78,103 +171,24 @@ interface SongStore { Song::class, ] ) - fun valuesAsFlowRaw(query: SupportSQLiteQuery): Flow> -} + protected abstract fun valuesAsFlow(query: SupportSQLiteQuery): Flow> -internal fun SongStore.valuesQuery( - sortBy: SongRepository.SortBy, - sortReverse: Boolean, - additionalClauseAfterSongSelect: String = "", - additionalClauseBeforeJoins: String = "", - additionalArgsBeforeJoins: Array = emptyArray(), - overrideOrderBy: String? = null, -): SupportSQLiteQuery { - val aliasFirstAlbumArtist = "firstAlbumArtist" - val embeddedFirstArtistName = "firstArtistName" - val embeddedFirstAlbumName = "firstAlbumName" - val embeddedFirstAlbumArtistName = "firstAlbumArtistName" - val embeddedFirstComposerName = "firstComposerName" - val orderBy = overrideOrderBy ?: when (sortBy) { - SongRepository.SortBy.CUSTOM -> "${Song.TABLE}.${Song.COLUMN_ID}" - SongRepository.SortBy.TITLE -> "${Song.TABLE}.${Song.COLUMN_ID}" - SongRepository.SortBy.ARTIST -> embeddedFirstArtistName - SongRepository.SortBy.ALBUM -> embeddedFirstAlbumName - SongRepository.SortBy.DURATION -> "${Song.TABLE}.${Song.COLUMN_ID}" - SongRepository.SortBy.DATE_MODIFIED -> "${Song.TABLE}.${Song.COLUMN_ID}" - SongRepository.SortBy.COMPOSER -> embeddedFirstComposerName - SongRepository.SortBy.ALBUM_ARTIST -> embeddedFirstAlbumArtistName - SongRepository.SortBy.YEAR -> "${Song.TABLE}.${Song.COLUMN_YEAR}" - SongRepository.SortBy.FILENAME -> "${Song.TABLE}.${Song.COLUMN_FILENAME}" - SongRepository.SortBy.TRACK_NUMBER -> "${Song.TABLE}.${Song.COLUMN_TRACK_NUMBER}" + fun valuesAsFlow( + sortBy: SongRepository.SortBy, + sortReverse: Boolean, + additionalClauseAfterSongSelect: String = "", + additionalClauseBeforeJoins: String = "", + additionalArgsBeforeJoins: Array = emptyArray(), + overrideOrderBy: String? = null, + ): Flow> { + val query = valuesQuery( + sortBy = sortBy, + sortReverse = sortReverse, + additionalClauseAfterSongSelect = additionalClauseAfterSongSelect, + additionalClauseBeforeJoins = additionalClauseBeforeJoins, + additionalArgsBeforeJoins = additionalArgsBeforeJoins, + overrideOrderBy = overrideOrderBy, + ) + return valuesAsFlow(query) } - val orderDirection = if (sortReverse) "DESC" else "ASC" - val artistQuery = "SELECT" + - "TOP 1 ${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_ARTIST_ID} " + - "FROM ${ArtistSongMapping.TABLE} " + - "WHERE ${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_SONG_ID} = ${Song.COLUMN_ID}" - val albumQuery = "SELECT" + - "TOP 1 ${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_ALBUM_ID} " + - "FROM ${AlbumSongMapping.TABLE} " + - "WHERE ${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_SONG_ID} = ${Song.COLUMN_ID}" - val albumArtistQuery = "SELECT " + - "TOP 1 ${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ARTIST_ID} " + - "FROM ${AlbumArtistMapping.TABLE} " + - "WHERE ${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID} = ${Album.COLUMN_ID}" - val composerQuery = "SELECT " + - "TOP 1 ${ComposerSongMapping.TABLE}.${ComposerSongMapping.COLUMN_COMPOSER_ID} " + - "FROM ${ComposerSongMapping.TABLE} " + - "WHERE ${ComposerSongMapping.TABLE}.${ComposerSongMapping.COLUMN_SONG_ID} = ${Song.COLUMN_ID}" - val query = "SELECT ${Song.TABLE}.*, " + - additionalClauseAfterSongSelect + - "${Artist.TABLE}.${Artist.COLUMN_NAME} as $embeddedFirstArtistName, " + - "${Album.TABLE}.${Album.COLUMN_NAME} as $embeddedFirstAlbumName, " + - "$aliasFirstAlbumArtist.${Artist.COLUMN_NAME} as $embeddedFirstAlbumArtistName, " + - "${Composer.TABLE}.${Composer.COLUMN_NAME} as $embeddedFirstComposerName " + - "FROM ${Song.TABLE} " + - additionalClauseBeforeJoins + - "LEFT JOIN ${Artist.TABLE} ON ${Artist.TABLE}.${Artist.COLUMN_ID} = ($artistQuery)" + - "LEFT JOIN ${Album.TABLE} ON ${Album.TABLE}.${Album.COLUMN_ID} = ($albumQuery)" + - "LEFT JOIN ${Artist.TABLE} $aliasFirstAlbumArtist ON ${Artist.TABLE}.${Artist.COLUMN_ID} = ($albumArtistQuery)" + - "LEFT JOIN ${Composer.TABLE} ON ${Composer.TABLE}.${Composer.COLUMN_ID} = ($composerQuery)" + - "ORDER BY $orderBy $orderDirection" - val args = additionalArgsBeforeJoins - return SimpleSQLiteQuery(query, args) -} - -fun SongStore.values( - sortBy: SongRepository.SortBy, - sortReverse: Boolean, - additionalClauseAfterSongSelect: String = "", - additionalClauseBeforeJoins: String = "", - additionalArgsBeforeJoins: Array = emptyArray(), - overrideOrderBy: String? = null, -): List { - val query = valuesQuery( - sortBy = sortBy, - sortReverse = sortReverse, - additionalClauseAfterSongSelect = additionalClauseAfterSongSelect, - additionalClauseBeforeJoins = additionalClauseBeforeJoins, - additionalArgsBeforeJoins = additionalArgsBeforeJoins, - overrideOrderBy = overrideOrderBy, - ) - return valuesRaw(query) -} - -fun SongStore.valuesAsFlow( - sortBy: SongRepository.SortBy, - sortReverse: Boolean, - additionalClauseAfterSongSelect: String = "", - additionalClauseBeforeJoins: String = "", - additionalArgsBeforeJoins: Array = emptyArray(), - overrideOrderBy: String? = null, -): Flow> { - val query = valuesQuery( - sortBy = sortBy, - sortReverse = sortReverse, - additionalClauseAfterSongSelect = additionalClauseAfterSongSelect, - additionalClauseBeforeJoins = additionalClauseBeforeJoins, - additionalArgsBeforeJoins = additionalArgsBeforeJoins, - overrideOrderBy = overrideOrderBy, - ) - return valuesAsFlowRaw(query) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/MediaExposer.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/MediaExposer.kt index ed0680b1..38f52ed2 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/MediaExposer.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/MediaExposer.kt @@ -20,13 +20,13 @@ import io.github.zyrouge.symphony.services.groove.entities.PlaylistSongMapping import io.github.zyrouge.symphony.services.groove.entities.Song import io.github.zyrouge.symphony.services.groove.entities.SongArtworkIndex import io.github.zyrouge.symphony.services.groove.entities.SongLyric -import io.github.zyrouge.symphony.utils.ActivityUtils +import io.github.zyrouge.symphony.utils.ActivityHelper import io.github.zyrouge.symphony.utils.ConcurrentSet import io.github.zyrouge.symphony.utils.DocumentFileX import io.github.zyrouge.symphony.utils.ImagePreserver import io.github.zyrouge.symphony.utils.Logger import io.github.zyrouge.symphony.utils.SimplePath -import io.github.zyrouge.symphony.utils.concurrentSetOf +import io.github.zyrouge.symphony.utils.builtin.concurrentSetOf import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -62,7 +62,7 @@ class MediaExposer(private val symphony: Symphony) { val folderUris = symphony.settings.mediaFolders.value val scanner = MediaTreeScanner.create(symphony) folderUris.map { x -> - ActivityUtils.makePersistableReadableUri(context, x) + ActivityHelper.makePersistableReadableUri(context, x) DocumentFileX.fromTreeUri(context, x)?.let { val path = SimplePath(DocumentFileX.getParentPathOfTreeUri(x) ?: it.name) with(Dispatchers.IO) { @@ -86,7 +86,7 @@ class MediaExposer(private val symphony: Symphony) { val playlistId = exPlaylist.id val uri = exPlaylist.uri!! playlistIdsToBeDeletedInMapping.add(playlistId) - ActivityUtils.makePersistableReadableUri(symphony.applicationContext, uri) + ActivityHelper.makePersistableReadableUri(symphony.applicationContext, uri) val extended = Playlist.parse(symphony, playlistId, uri) playlistsToBeUpdated.add(extended.playlist) var nextPlaylistSongMapping: PlaylistSongMapping? = null diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueue.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueue.kt index 728985ef..8273dc54 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueue.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueue.kt @@ -5,6 +5,7 @@ import androidx.room.ColumnInfo import androidx.room.Embedded import androidx.room.Entity import androidx.room.ForeignKey +import androidx.room.Index import androidx.room.PrimaryKey @Immutable @@ -18,19 +19,32 @@ import androidx.room.PrimaryKey onDelete = ForeignKey.SET_NULL, ), ], + indices = [Index(SongQueue.COLUMN_INTERNAL_ID, unique = true)], ) data class SongQueue( @PrimaryKey @ColumnInfo(COLUMN_ID) val id: String, + @ColumnInfo(COLUMN_INTERNAL_ID) + val internalId: Int? = null, @ColumnInfo(COLUMN_PLAYING_ID) val playingId: String?, @ColumnInfo(COLUMN_PLAYING_TIMESTAMP) val playingTimestamp: Long?, + @ColumnInfo(COLUMN_PLAYING_SPEED_INT) + val playingSpeedInt: Int, + @ColumnInfo(COLUMN_PLAYING_PITCH_INT) + val playingPitchInt: Int, @ColumnInfo(COLUMN_SHUFFLED) val shuffled: Boolean, @ColumnInfo(COLUMN_LOOP_MODE) val loopMode: LoopMode, + @ColumnInfo(COLUMN_SPEED_INT) + val speedInt: Int, + @ColumnInfo(COLUMN_PITCH_INT) + val pitchInt: Int, + @ColumnInfo(COLUMN_PAUSE_ON_SONG_END) + val pauseOnSongEnd: Boolean, ) { enum class LoopMode { None, @@ -53,12 +67,24 @@ data class SongQueue( } } + val speed get() = speedInt.toFloat() / SPEED_MULTIPLIER + val pitch get() = pitchInt.toFloat() / PITCH_MULTIPLIER + companion object { const val TABLE = "song_queue" const val COLUMN_ID = "id" + const val COLUMN_INTERNAL_ID = "internal_id" const val COLUMN_PLAYING_ID = "playing_id" + const val COLUMN_PLAYING_SPEED_INT = "playing_speed_int" + const val COLUMN_PLAYING_PITCH_INT = "playing_pitch_int" const val COLUMN_PLAYING_TIMESTAMP = "playing_timestamp" const val COLUMN_SHUFFLED = "shuffled" const val COLUMN_LOOP_MODE = "loop_mode" + const val COLUMN_SPEED_INT = "speed_int" + const val COLUMN_PITCH_INT = "pitch_int" + const val COLUMN_PAUSE_ON_SONG_END = "pause_on_song_end" + + const val SPEED_MULTIPLIER = 100 + const val PITCH_MULTIPLIER = 100 } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueueSongMapping.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueueSongMapping.kt index 60e0922a..03d1d86b 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueueSongMapping.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueueSongMapping.kt @@ -29,11 +29,18 @@ import androidx.room.PrimaryKey childColumns = arrayOf(SongQueueSongMapping.COLUMN_NEXT_ID), onDelete = ForeignKey.SET_NULL, ), + ForeignKey( + entity = SongQueueSongMapping::class, + parentColumns = arrayOf(SongQueueSongMapping.COLUMN_ID), + childColumns = arrayOf(SongQueueSongMapping.COLUMN_OG_NEXT_ID), + onDelete = ForeignKey.SET_NULL, + ), ], indices = [ Index(SongQueueSongMapping.COLUMN_QUEUE_ID), Index(SongQueueSongMapping.COLUMN_IS_HEAD), Index(SongQueueSongMapping.COLUMN_NEXT_ID), + Index(SongQueueSongMapping.COLUMN_OG_NEXT_ID), ], ) data class SongQueueSongMapping( @@ -48,6 +55,8 @@ data class SongQueueSongMapping( val isHead: Boolean, @ColumnInfo(COLUMN_NEXT_ID) val nextId: String?, + @ColumnInfo(COLUMN_OG_NEXT_ID) + val ogNextId: String?, ) { companion object { const val TABLE = "song_queue_songs_mapping" @@ -56,5 +65,6 @@ data class SongQueueSongMapping( const val COLUMN_SONG_ID = "song_id" const val COLUMN_IS_HEAD = "is_head" const val COLUMN_NEXT_ID = "next_id" + const val COLUMN_OG_NEXT_ID = "og_next_id" } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/radio/Radio.kt b/app/src/main/java/io/github/zyrouge/symphony/services/radio/Radio.kt index f1584b59..42d6004d 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/radio/Radio.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/radio/Radio.kt @@ -1,41 +1,13 @@ package io.github.zyrouge.symphony.services.radio import io.github.zyrouge.symphony.Symphony -import io.github.zyrouge.symphony.utils.Eventer +import io.github.zyrouge.symphony.services.groove.entities.Song import io.github.zyrouge.symphony.utils.Logger -import kotlinx.coroutines.launch import java.time.Instant import java.util.Date import java.util.Timer class Radio(private val symphony: Symphony) : Symphony.Hooks { - sealed class Events { - sealed class Player : Events() { - object Staged : Player() - object Started : Player() - object Stopped : Player() - object Paused : Player() - object Resumed : Player() - object Seeked : Player() - object Ended : Player() - } - - sealed class Queue : Events() { - object Modified : Queue() - object IndexChanged : Queue() - object Cleared : Queue() - } - - sealed class QueueOption : Events() { - object LoopModeChanged : QueueOption() - object ShuffleModeChanged : QueueOption() - object SleepTimerChanged : QueueOption() - object SpeedChanged : QueueOption() - object PitchChanged : QueueOption() - object PauseOnCurrentSongEndChanged : QueueOption() - } - } - data class SleepTimer( val duration: Long, val endsAt: Long, @@ -43,72 +15,63 @@ class Radio(private val symphony: Symphony) : Symphony.Hooks { var quitOnEnd: Boolean, ) - val onUpdate = Eventer() val queue = RadioQueue(symphony) val shorty = RadioShorty(symphony) val session = RadioSession(symphony) - var observatory = RadioObservatory(symphony) private val focus = RadioFocus(symphony) private val nativeReceiver = RadioNativeReceiver(symphony) private var player: RadioPlayer? = null private var nextPlayer: RadioPlayer? = null - val hasPlayer get() = player?.usable == true - val isPlaying get() = player?.isPlaying == true - val currentPlaybackPosition get() = player?.playbackPosition - val currentSpeed get() = player?.speed ?: RadioPlayer.DEFAULT_SPEED - val currentPitch get() = player?.pitch ?: RadioPlayer.DEFAULT_PITCH - val audioSessionId get() = player?.audioSessionId - val onPlaybackPositionUpdate = Eventer() - - var persistedSpeed = RadioPlayer.DEFAULT_SPEED - var persistedPitch = RadioPlayer.DEFAULT_PITCH - var sleepTimer: SleepTimer? = null - var pauseOnCurrentSongEnd = false - init { nativeReceiver.start() - onUpdate.subscribe(this::watchQueueUpdates) } fun ready() { - attachGrooveListener() session.start() - observatory.start() } fun destroy() { stop() - observatory.destroy() session.destroy() nativeReceiver.destroy() } data class PlayOptions( - val index: Int = 0, + val songMappingId: String? = null, val autostart: Boolean = true, val startPosition: Long? = null, ) - fun play(options: PlayOptions) { + suspend fun play(options: PlayOptions) { stopCurrentSong() - val song = queue.getSongIdAt(options.index)?.let { symphony.groove.song.get(it) } + // TODO: can queue be nullable? + val queue = + symphony.database.songQueue.findByInternalId(RadioQueue.SONG_QUEUE_INTERNAL_ID_DEFAULT) + if (queue == null) { + onSongFinish(SongFinishSource.Exception) + return + } + val song = options.songMappingId + ?.let { symphony.database.songQueueSongMapping.findById(queue.entity.id, it) } + ?: symphony.database.songQueueSongMapping.findHead(queue.entity.id) if (song == null) { onSongFinish(SongFinishSource.Exception) return } try { - queue.currentSongIndex = options.index + val nQueue = queue.entity.copy(playingId = song.mapping.id) + symphony.database.songQueue.update(nQueue) player = nextPlayer?.takeIf { when { - it.id == song.id -> true + it.id == song.entity.id -> true else -> { it.destroy() false } } - } ?: RadioPlayer(symphony, song.id, song.uri) + } ?: RadioPlayer(symphony, song.entity.id, song.entity.uri) nextPlayer = null player!!.setOnPreparedListener { options.startPosition?.let { @@ -116,14 +79,15 @@ class Radio(private val symphony: Symphony) : Symphony.Hooks { seek(it) } } - setSpeed(persistedSpeed, true) - setPitch(persistedPitch, true) + setSpeed(queue.entity.speed, true) + setPitch(queue.entity.pitch, true) if (options.autostart) { start() } } player!!.setOnPlaybackPositionListener { - onPlaybackPositionUpdate.dispatch(it) + // TODO + // onPlaybackPositionUpdate.dispatch(it) } player!!.setOnFinishListener { onSongFinish(SongFinishSource.Finish) @@ -131,28 +95,41 @@ class Radio(private val symphony: Symphony) : Symphony.Hooks { player!!.setOnErrorListener { what, extra -> Logger.warn( "Radio", - "skipping song ${queue.currentSongId} (${queue.currentSongIndex}) due to $what + $extra" + "skipping song ${song.entity.id} (${song.mapping.id}) due to $what + $extra" ) when { // happens when change playback params fail, we skip it since its non-critical what == 1 && extra == -22 -> onSongFinish(SongFinishSource.Finish) else -> { - queue.remove(queue.currentSongIndex) + removeFromQueue(queue.entity.id, song) onSongFinish(SongFinishSource.Exception) } } } player!!.prepare() prepareNextPlayer() - onUpdate.dispatch(Events.Player.Staged) } catch (err: Exception) { Logger.warn( "Radio", - "skipping song ${queue.currentSongId} (${queue.currentSongIndex})", + "skipping song ${song.entity.id} (${song.mapping.id})", err, ) - queue.remove(queue.currentSongIndex) + removeFromQueue(queue.entity.id, song) + } + } + + private suspend fun removeFromQueue(queueId: String, song: Song.AlongSongQueueMapping) { + val previousSong = + symphony.database.songQueueSongMapping.findByNextId(queueId, song.mapping.id) + if (previousSong == null) { + // TODO: handle this + return } + val nPreviousSongMapping = previousSong.mapping.copy( + nextId = song.mapping.nextId, + ogNextId = song.mapping.ogNextId, + ) + symphony.database.songQueueSongMapping.delete(queueId, listOf(song.mapping.id)) } private fun prepareNextPlayer() { @@ -191,12 +168,6 @@ class Radio(private val symphony: Symphony) : Symphony.Hooks { } it.changeVolume(RadioPlayer.MAX_VOLUME) {} it.start() - onUpdate.dispatch( - when { - !it.hasPlayedOnce -> Events.Player.Started - else -> Events.Player.Resumed - } - ) } } @@ -214,16 +185,12 @@ class Radio(private val symphony: Symphony) : Symphony.Hooks { it.pause() focus.abandonFocus() onFinish() - onUpdate.dispatch(Events.Player.Paused) } } } fun pauseInstant() { - player?.let { - it.pause() - onUpdate.dispatch(Events.Player.Paused) - } + player?.pause() } fun stop(ended: Boolean = true) { @@ -381,53 +348,6 @@ class Radio(private val symphony: Symphony) : Symphony.Hooks { return nextSongIndex to autostart } - private fun attachGrooveListener() { - symphony.groove.coroutineScope.launch { - symphony.groove.readyDeferred.await() - restorePreviousQueue() - } - } - - private fun restorePreviousQueue() { - if (!queue.isEmpty()) { - return - } - symphony.settings.previousSongQueue.value?.let { previous -> - var currentSongIndex = previous.currentSongIndex - var playedDuration = previous.playedDuration - val originalQueue = mutableListOf() - val currentQueue = mutableListOf() - previous.originalQueue.forEach { songId -> - if (symphony.groove.song.get(songId) != null) { - originalQueue.add(songId) - } - } - previous.currentQueue.forEachIndexed { i, songId -> - if (symphony.groove.song.get(songId) != null) { - currentQueue.add(songId) - } else { - if (i < currentSongIndex) currentSongIndex-- - } - } - if (originalQueue.isEmpty() || hasPlayer) { - return@let - } - if (currentSongIndex >= originalQueue.size) { - currentSongIndex = 0 - playedDuration = 0 - } - queue.restore( - RadioQueue.Serialized( - currentSongIndex = currentSongIndex, - playedDuration = playedDuration, - originalQueue = originalQueue, - currentQueue = currentQueue, - shuffled = previous.shuffled, - ) - ) - } - } - internal fun watchQueueUpdates(event: Events) { if (event !is Events.Queue) { return @@ -443,24 +363,4 @@ class Radio(private val symphony: Symphony) : Symphony.Hooks { saveCurrentQueue() destroy() } - - override fun onSymphonyActivityPause() { - saveCurrentQueue() - } - - override fun onSymphonyActivityDestroy() { - saveCurrentQueue() - } - - private fun saveCurrentQueue() { - if (queue.isEmpty()) { - return - } - symphony.settings.previousSongQueue.setValue( - RadioQueue.Serialized.create( - queue = queue, - playbackPosition = currentPlaybackPosition ?: RadioPlayer.PlaybackPosition.zero - ) - ) - } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioQueue.kt b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioQueue.kt index c99fc10a..171605c5 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioQueue.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioQueue.kt @@ -2,88 +2,102 @@ package io.github.zyrouge.symphony.services.radio import io.github.zyrouge.symphony.Symphony import io.github.zyrouge.symphony.services.groove.entities.Song -import io.github.zyrouge.symphony.utils.concurrentListOf +import io.github.zyrouge.symphony.services.groove.entities.SongQueue +import io.github.zyrouge.symphony.services.groove.entities.SongQueueSongMapping class RadioQueue(private val symphony: Symphony) { - enum class LoopMode { - None, - Queue, - Song; - - companion object { - val values = enumValues() - } - } - - val originalQueue = concurrentListOf() - val currentQueue = concurrentListOf() - - var currentSongIndex = -1 - internal set(value) { - field = value - symphony.radio.onUpdate.dispatch(Radio.Events.Queue.IndexChanged) - } - - var currentShuffleMode = false - private set(value) { - field = value - symphony.radio.onUpdate.dispatch(Radio.Events.QueueOption.ShuffleModeChanged) - } - - var currentLoopMode = LoopMode.None - private set(value) { - field = value - symphony.radio.onUpdate.dispatch(Radio.Events.QueueOption.LoopModeChanged) - } - - val currentSongId: String? - get() = getSongIdAt(currentSongIndex) - - fun hasSongAt(index: Int) = index > -1 && index < currentQueue.size - fun getSongIdAt(index: Int) = if (hasSongAt(index)) currentQueue[index] else null - - fun reset() { - originalQueue.clear() - currentQueue.clear() - currentSongIndex = -1 - symphony.radio.onUpdate.dispatch(Radio.Events.Queue.Cleared) +// val queueFlow = symphony.database.songQueue.findFirstAsFlow() +// val queue = AtomicReference(null) + +// @OptIn(ExperimentalCoroutinesApi::class) +// val queueSongsFlow = queueFlow.transformLatest { +// if (it == null) { +// emit(emptyList()) +// return@transformLatest +// } +// emitAll(symphony.database.songQueueSongMapping.valuesAsFlow(it.entity.id)) +// } +// val queueSongs = AtomicReference>(emptyList()) + + init { +// symphony.groove.coroutineScope.launch { +// queueFlow.collect { +// queue.set(it) +// } +// } +// symphony.groove.coroutineScope.launch { +// queueSongsFlow.collect { +// queueSongs.set(it) +// } +// } } - fun add( + suspend fun add( songIds: List, - index: Int? = null, + previousSongMappingId: String? = null, options: Radio.PlayOptions = Radio.PlayOptions(), ) { - index?.let { - originalQueue.addAll(it, songIds) - currentQueue.addAll(it, songIds) - if (it <= currentSongIndex) { - currentSongIndex += songIds.size - } - } ?: run { - originalQueue.addAll(songIds) - currentQueue.addAll(songIds) + val origQueue = symphony.database.songQueue.findByInternalId(SONG_QUEUE_INTERNAL_ID_DEFAULT) + val queueId = origQueue?.entity?.id ?: symphony.database.songQueueIdGenerator.next() + if (origQueue == null) { + val queue = SongQueue( + id = queueId, + playingId = null, + playingTimestamp = null, + playingSpeedInt = SongQueue.SPEED_MULTIPLIER, + playingPitchInt = SongQueue.PITCH_MULTIPLIER, + shuffled = false, + loopMode = SongQueue.LoopMode.None, + speedInt = SongQueue.SPEED_MULTIPLIER, + pitchInt = SongQueue.PITCH_MULTIPLIER, + pauseOnSongEnd = false, + ) + symphony.database.songQueue.insert(queue) } + var previousSong = previousSongMappingId?.let { + symphony.database.songQueueSongMapping.findById(queueId, it) + } + var nextMappingId = previousSong?.mapping?.nextId + var ogNextMappingId = previousSong?.mapping?.ogNextId + val added = mutableListOf() + var i = 0 + val songIdsCount = songIds.size + for (x in songIds.reversed()) { + val isHead = origQueue == null && i == songIdsCount - 1 + val mapping = SongQueueSongMapping( + id = symphony.database.songQueueSongMappingIdGenerator.next(), + queueId = queueId, + songId = x, + isHead = isHead, + nextId = nextMappingId, + ogNextId = ogNextMappingId, + ) + added.add(mapping) + nextMappingId = mapping.id + ogNextMappingId = mapping.id + i++ + } + symphony.database.songQueueSongMapping.insert(*added.toTypedArray()) afterAdd(options) } - fun add( + suspend fun add( songId: String, - index: Int? = null, + previousSongId: String? = null, options: Radio.PlayOptions = Radio.PlayOptions(), - ) = add(listOf(songId), index, options) + ) = add(listOf(songId), previousSongId, options) - fun add( + suspend fun add( songs: List, - index: Int? = null, + previousSongId: String? = null, options: Radio.PlayOptions = Radio.PlayOptions(), - ) = add(songs.map { it.id }, index, options) + ) = add(songs.map { it.id }, previousSongId, options) - fun add( + suspend fun add( song: Song, - index: Int? = null, + previousSongId: String? = null, options: Radio.PlayOptions = Radio.PlayOptions(), - ) = add(listOf(song.id), index, options) + ) = add(listOf(song.id), previousSongId, options) private fun afterAdd(options: Radio.PlayOptions) { if (!symphony.radio.hasPlayer) { @@ -92,7 +106,7 @@ class RadioQueue(private val symphony: Symphony) { symphony.radio.onUpdate.dispatch(Radio.Events.Queue.Modified) } - fun remove(index: Int) { + fun remove(id: String) { originalQueue.removeAt(index) currentQueue.removeAt(index) symphony.radio.onUpdate.dispatch(Radio.Events.Queue.Modified) @@ -155,67 +169,7 @@ class RadioQueue(private val symphony: Symphony) { symphony.radio.onUpdate.dispatch(Radio.Events.Queue.Modified) } - fun isEmpty() = originalQueue.isEmpty() - - data class Serialized( - val currentSongIndex: Int, - val playedDuration: Long, - val originalQueue: List, - val currentQueue: List, - val shuffled: Boolean, - ) { - fun serialize() = - listOf( - currentSongIndex.toString(), - playedDuration.toString(), - originalQueue.joinToString(","), - currentQueue.joinToString(","), - shuffled.toString(), - ).joinToString(";") - - companion object { - fun create(queue: RadioQueue, playbackPosition: RadioPlayer.PlaybackPosition) = - Serialized( - currentSongIndex = queue.currentSongIndex, - playedDuration = playbackPosition.played, - originalQueue = queue.originalQueue.toList(), - currentQueue = queue.currentQueue.toList(), - shuffled = queue.currentShuffleMode, - ) - - fun parse(data: String): Serialized? { - try { - val semi = data.split(";") - return Serialized( - currentSongIndex = semi[0].toInt(), - playedDuration = semi[1].toLong(), - originalQueue = semi[2].split(","), - currentQueue = semi[3].split(","), - shuffled = semi[4].toBoolean(), - ) - } catch (_: Exception) { - } - return null - } - } - } - - fun restore(serialized: Serialized) { - if (serialized.originalQueue.isNotEmpty()) { - symphony.radio.stop(ended = false) - originalQueue.clear() - originalQueue.addAll(serialized.originalQueue) - currentQueue.clear() - currentQueue.addAll(serialized.currentQueue) - symphony.radio.onUpdate.dispatch(Radio.Events.Queue.Modified) - currentShuffleMode = serialized.shuffled - afterAdd( - Radio.PlayOptions( - index = serialized.currentSongIndex, - autostart = false, - startPosition = serialized.playedDuration, - ) - ) - } + companion object { + const val SONG_QUEUE_INTERNAL_ID_DEFAULT = 1 } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/AddToPlaylistDialog.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/AddToPlaylistDialog.kt index a24e8409..04dd9f28 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/AddToPlaylistDialog.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/AddToPlaylistDialog.kt @@ -20,7 +20,7 @@ import androidx.compose.runtime.toMutableStateList import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.utils.mutate +import io.github.zyrouge.symphony.utils.builtin.mutate @Composable fun AddToPlaylistDialog( diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/LazyColumnScrollBar.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/LazyColumnScrollBar.kt index 3311c5ff..54afdd09 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/LazyColumnScrollBar.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/LazyColumnScrollBar.kt @@ -14,7 +14,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.draw.drawWithContent -import io.github.zyrouge.symphony.utils.toSafeFinite +import io.github.zyrouge.symphony.utils.builtin.toSafeFinite fun Modifier.drawScrollBar(state: LazyListState): Modifier = composed { val scrollPointerColor = MaterialTheme.colorScheme.surfaceTint diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/LazyGridScrollBar.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/LazyGridScrollBar.kt index e416f11c..c51c685a 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/LazyGridScrollBar.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/LazyGridScrollBar.kt @@ -13,7 +13,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.draw.drawWithContent -import io.github.zyrouge.symphony.utils.toSafeFinite +import io.github.zyrouge.symphony.utils.builtin.toSafeFinite import kotlin.math.floor fun Modifier.drawScrollBar(state: LazyGridState, columns: Int): Modifier = composed { diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/LongPressCopyableText.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/LongPressCopyableText.kt index 28797bae..7a5febe8 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/LongPressCopyableText.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/LongPressCopyableText.kt @@ -6,7 +6,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.pointerInput import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.utils.ActivityUtils +import io.github.zyrouge.symphony.utils.ActivityHelper @Composable fun LongPressCopyableText(context: ViewContext, text: String) { @@ -14,7 +14,7 @@ fun LongPressCopyableText(context: ViewContext, text: String) { text, modifier = Modifier.pointerInput(Unit) { detectTapGestures(onLongPress = { - ActivityUtils.copyToClipboardAndNotify(context.symphony, text) + ActivityHelper.copyToClipboardAndNotify(context.symphony, text) }) } ) diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/NowPlayingBottomBar.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/NowPlayingBottomBar.kt index ae363a91..1206c5e8 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/NowPlayingBottomBar.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/NowPlayingBottomBar.kt @@ -66,7 +66,7 @@ import io.github.zyrouge.symphony.ui.helpers.FadeTransition import io.github.zyrouge.symphony.ui.helpers.TransitionDurations import io.github.zyrouge.symphony.ui.helpers.ViewContext import io.github.zyrouge.symphony.ui.view.NowPlayingViewRoute -import io.github.zyrouge.symphony.utils.runIfOrThis +import io.github.zyrouge.symphony.utils.builtin.runIfOrThis import kotlin.math.absoluteValue @Composable diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/Slider.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/Slider.kt index 79617078..f4f48d0d 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/Slider.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/Slider.kt @@ -21,7 +21,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import io.github.zyrouge.symphony.utils.RangeUtils +import io.github.zyrouge.symphony.utils.RangeCalculator @Composable fun Slider( @@ -32,7 +32,7 @@ fun Slider( onChange: (Float) -> Unit, ) { Column(modifier = modifier) { - val ratio = RangeUtils.calculateRatioFromValue(value, range) + val ratio = RangeCalculator.calculateRatioFromValue(value, range) var pointerOffsetX = 0f BoxWithConstraints(modifier = Modifier.padding(20.dp, 0.dp)) { @@ -53,7 +53,7 @@ fun Slider( pointerOffsetX = offset.x val widthPx = this@BoxWithConstraints.maxWidth.toPx() val nRatio = (pointerOffsetX / widthPx).coerceIn(0f..1f) - val nValue = RangeUtils.calculateValueFromRatio(nRatio, range) + val nValue = RangeCalculator.calculateValueFromRatio(nRatio, range) onChange(nValue) } ) @@ -69,7 +69,7 @@ fun Slider( val widthPx = maxWidth.toPx() val nRatio = (pointerOffsetX / widthPx).coerceIn(0f..1f) val nValue = - RangeUtils.calculateValueFromRatio(nRatio, range) + RangeCalculator.calculateValueFromRatio(nRatio, range) onChange(nValue) }, ) diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongInformationDialog.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongInformationDialog.kt index ad569fb2..20147d57 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongInformationDialog.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongInformationDialog.kt @@ -15,8 +15,8 @@ import io.github.zyrouge.symphony.ui.view.AlbumArtistViewRoute import io.github.zyrouge.symphony.ui.view.AlbumViewRoute import io.github.zyrouge.symphony.ui.view.ArtistViewRoute import io.github.zyrouge.symphony.ui.view.GenreViewRoute -import io.github.zyrouge.symphony.utils.ActivityUtils -import io.github.zyrouge.symphony.utils.DurationUtils +import io.github.zyrouge.symphony.utils.ActivityHelper +import io.github.zyrouge.symphony.utils.DurationHelper import java.text.SimpleDateFormat import java.util.Date import kotlin.math.round @@ -104,7 +104,7 @@ fun SongInformationDialog(context: ViewContext, song: Song, onDismissRequest: () } } InformationKeyValue(context.symphony.t.Duration) { - LongPressCopyableText(context, DurationUtils.formatMs(song.duration)) + LongPressCopyableText(context, DurationHelper.formatMs(song.duration)) } song.encoder?.let { InformationKeyValue(context.symphony.t.Encoder) { @@ -168,7 +168,7 @@ private fun LongPressCopyableAndTappableText( modifier = Modifier.pointerInput(Unit) { detectTapGestures( onLongPress = { _ -> - ActivityUtils.copyToClipboardAndNotify(context.symphony, it) + ActivityHelper.copyToClipboardAndNotify(context.symphony, it) }, onTap = { _ -> onTap(it) diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongTreeList.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongTreeList.kt index fe3ef2dc..0bf9c63d 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongTreeList.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongTreeList.kt @@ -58,7 +58,7 @@ import io.github.zyrouge.symphony.services.groove.repositories.SongRepository import io.github.zyrouge.symphony.services.radio.Radio import io.github.zyrouge.symphony.ui.helpers.ViewContext import io.github.zyrouge.symphony.utils.SimplePath -import io.github.zyrouge.symphony.utils.StringListUtils +import io.github.zyrouge.symphony.utils.StringSorter @Composable fun SongTreeList( @@ -82,7 +82,7 @@ fun SongTreeList( val songsSortReverse by context.symphony.settings.lastUsedSongsSortReverse.flow.collectAsStateWithLifecycle() val sortedTree by remember(tree, pathsSortBy, pathsSortReverse, songsSortBy, songsSortReverse) { derivedStateOf { - val pairs = StringListUtils.sort(tree.keys.toList(), pathsSortBy, pathsSortReverse) + val pairs = StringSorter.sort(tree.keys.toList(), pathsSortBy, pathsSortReverse) .map { it to context.symphony.groove.song.sort( tree[it]!!, @@ -354,11 +354,11 @@ fun SongTreeListSongCardIconButton( private fun SongTreeListMediaSortBar( context: ViewContext, songsCount: Int, - pathsSortBy: StringListUtils.SortBy, + pathsSortBy: StringSorter.SortBy, pathsSortReverse: Boolean, songsSortBy: SongRepository.SortBy, songsSortReverse: Boolean, - setPathsSortBy: (StringListUtils.SortBy) -> Unit, + setPathsSortBy: (StringSorter.SortBy) -> Unit, setPathsSortReverse: (Boolean) -> Unit, setSongsSortBy: (SongRepository.SortBy) -> Unit, setSongsSortReverse: (Boolean) -> Unit, @@ -426,7 +426,7 @@ private fun SongTreeListMediaSortBar( style = currentTextStyle, modifier = Modifier.padding(16.dp, 8.dp), ) - StringListUtils.SortBy.entries.forEach { sortBy -> + StringSorter.SortBy.entries.forEach { sortBy -> SongTreeListMediaSortBarDropdownMenuItem( selected = pathsSortBy == sortBy, reversed = pathsSortReverse, @@ -513,9 +513,9 @@ private fun SongTreeListMediaSortBarDropdownMenuItem( ) } -fun StringListUtils.SortBy.label(context: ViewContext) = when (this) { - StringListUtils.SortBy.CUSTOM -> context.symphony.t.Custom - StringListUtils.SortBy.NAME -> context.symphony.t.Name +fun StringSorter.SortBy.label(context: ViewContext) = when (this) { + StringSorter.SortBy.CUSTOM -> context.symphony.t.Custom + StringSorter.SortBy.NAME -> context.symphony.t.Name } private fun createLinearTree( diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/TopAppBarMinimalTitle.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/TopAppBarMinimalTitle.kt index 55367ca0..1a658c49 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/TopAppBarMinimalTitle.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/TopAppBarMinimalTitle.kt @@ -12,7 +12,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import io.github.zyrouge.symphony.utils.runIfOrThis +import io.github.zyrouge.symphony.utils.builtin.runIfOrThis @Composable fun TopAppBarMinimalTitle( diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/settings/ConsiderContributingTile.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/settings/ConsiderContributingTile.kt index a5199432..2d5fe6f2 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/settings/ConsiderContributingTile.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/settings/ConsiderContributingTile.kt @@ -23,18 +23,18 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import io.github.zyrouge.symphony.services.AppMeta import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.utils.ActivityUtils +import io.github.zyrouge.symphony.utils.ActivityHelper @Composable fun ConsiderContributingTile(context: ViewContext) { val contentColor = MaterialTheme.colorScheme.onPrimary - + Box( modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.primary) .clickable { - ActivityUtils.startBrowserActivity( + ActivityHelper.startBrowserActivity( context.activity, Uri.parse(AppMeta.contributingUrl) ) diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/settings/LinkTile.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/settings/LinkTile.kt index b82b6f9a..8f11c82e 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/settings/LinkTile.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/settings/LinkTile.kt @@ -7,7 +7,7 @@ import androidx.compose.material3.ListItem import androidx.compose.material3.Text import androidx.compose.runtime.Composable import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.utils.ActivityUtils +import io.github.zyrouge.symphony.utils.ActivityHelper @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -20,7 +20,7 @@ fun SettingsLinkTile( Card( colors = SettingsTileDefaults.cardColors(), onClick = { - ActivityUtils.startBrowserActivity(context.activity, Uri.parse(url)) + ActivityHelper.startBrowserActivity(context.activity, Uri.parse(url)) } ) { ListItem( diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/settings/MultiSystemFolderTile.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/settings/MultiSystemFolderTile.kt index a5e1639c..9509b584 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/settings/MultiSystemFolderTile.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/settings/MultiSystemFolderTile.kt @@ -27,7 +27,7 @@ import io.github.zyrouge.symphony.ui.components.ScaffoldDialog import io.github.zyrouge.symphony.ui.components.ScaffoldDialogDefaults import io.github.zyrouge.symphony.ui.components.drawScrollBar import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.utils.ActivityUtils +import io.github.zyrouge.symphony.utils.ActivityHelper @Composable fun SettingsMultiSystemFolderTile( @@ -61,7 +61,7 @@ fun SettingsMultiSystemFolderTile( ActivityResultContracts.OpenDocumentTree() ) { uri -> uri?.let { _ -> - ActivityUtils.makePersistableReadableUri(context.symphony.applicationContext, uri) + ActivityHelper.makePersistableReadableUri(context.symphony.applicationContext, uri) values.add(uri) } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/helpers/Context.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/helpers/ViewContext.kt similarity index 100% rename from app/src/main/java/io/github/zyrouge/symphony/ui/helpers/Context.kt rename to app/src/main/java/io/github/zyrouge/symphony/ui/helpers/ViewContext.kt diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Album.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/AlbumView.kt similarity index 98% rename from app/src/main/java/io/github/zyrouge/symphony/ui/view/Album.kt rename to app/src/main/java/io/github/zyrouge/symphony/ui/view/AlbumView.kt index d744a323..a42e16f0 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Album.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/AlbumView.kt @@ -34,6 +34,7 @@ import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.zyrouge.symphony.services.groove.entities.Album import io.github.zyrouge.symphony.services.groove.entities.Artist import io.github.zyrouge.symphony.ui.components.AlbumDropdownMenu @@ -46,7 +47,7 @@ import io.github.zyrouge.symphony.ui.components.SongCardThumbnailLabelStyle import io.github.zyrouge.symphony.ui.components.SongList import io.github.zyrouge.symphony.ui.components.TopAppBarMinimalTitle import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.utils.DurationUtils +import io.github.zyrouge.symphony.utils.DurationHelper import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.emptyFlow @@ -214,7 +215,7 @@ private fun AlbumHero( CircleSeparator() } Text( - DurationUtils.formatMs(duration), + DurationHelper.formatMs(duration), style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Bold) ) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Artist.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/ArtistView.kt similarity index 99% rename from app/src/main/java/io/github/zyrouge/symphony/ui/view/Artist.kt rename to app/src/main/java/io/github/zyrouge/symphony/ui/view/ArtistView.kt index a098689c..c4fd693c 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Artist.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/ArtistView.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.zyrouge.symphony.services.groove.entities.Artist import io.github.zyrouge.symphony.ui.components.AlbumRow import io.github.zyrouge.symphony.ui.components.AnimatedNowPlayingBottomBar diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Base.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/BaseView.kt similarity index 88% rename from app/src/main/java/io/github/zyrouge/symphony/ui/view/Base.kt rename to app/src/main/java/io/github/zyrouge/symphony/ui/view/BaseView.kt index 84c679b1..0a553f61 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Base.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/BaseView.kt @@ -19,19 +19,19 @@ import io.github.zyrouge.symphony.ui.helpers.ScaleTransition import io.github.zyrouge.symphony.ui.helpers.SlideTransition import io.github.zyrouge.symphony.ui.helpers.ViewContext import io.github.zyrouge.symphony.ui.theme.SymphonyTheme -import io.github.zyrouge.symphony.ui.view.settings.AppearanceSettingsView import io.github.zyrouge.symphony.ui.view.settings.AppearanceSettingsViewRoute -import io.github.zyrouge.symphony.ui.view.settings.GrooveSettingsView import io.github.zyrouge.symphony.ui.view.settings.GrooveSettingsViewRoute -import io.github.zyrouge.symphony.ui.view.settings.HomePageSettingsView import io.github.zyrouge.symphony.ui.view.settings.HomePageSettingsViewRoute -import io.github.zyrouge.symphony.ui.view.settings.MiniPlayerSettingsView import io.github.zyrouge.symphony.ui.view.settings.MiniPlayerSettingsViewRoute -import io.github.zyrouge.symphony.ui.view.settings.NowPlayingSettingsView import io.github.zyrouge.symphony.ui.view.settings.NowPlayingSettingsViewRoute -import io.github.zyrouge.symphony.ui.view.settings.PlayerSettingsView import io.github.zyrouge.symphony.ui.view.settings.PlayerSettingsViewRoute -import io.github.zyrouge.symphony.ui.view.settings.UpdateSettingsView +import io.github.zyrouge.symphony.ui.view.settings.SettingsAppearanceView +import io.github.zyrouge.symphony.ui.view.settings.SettingsGrooveView +import io.github.zyrouge.symphony.ui.view.settings.SettingsHomePageView +import io.github.zyrouge.symphony.ui.view.settings.SettingsMiniPlayerView +import io.github.zyrouge.symphony.ui.view.settings.SettingsNowPlayingView +import io.github.zyrouge.symphony.ui.view.settings.SettingsPlayerView +import io.github.zyrouge.symphony.ui.view.settings.SettingsUpdateView import io.github.zyrouge.symphony.ui.view.settings.UpdateSettingsViewRoute import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.serializer @@ -84,25 +84,25 @@ fun BaseView(symphony: Symphony, activity: MainActivity) { SettingsView(context, it.toRoute()) } baseComposable { - AppearanceSettingsView(context) + SettingsAppearanceView(context) } baseComposable { - GrooveSettingsView(context, it.toRoute()) + SettingsGrooveView(context, it.toRoute()) } baseComposable { - HomePageSettingsView(context) + SettingsHomePageView(context) } baseComposable { - MiniPlayerSettingsView(context) + SettingsMiniPlayerView(context) } baseComposable { - NowPlayingSettingsView(context) + SettingsNowPlayingView(context) } baseComposable { - PlayerSettingsView(context) + SettingsPlayerView(context) } baseComposable { - UpdateSettingsView(context) + SettingsUpdateView(context) } } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Genre.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/GenreView.kt similarity index 98% rename from app/src/main/java/io/github/zyrouge/symphony/ui/view/Genre.kt rename to app/src/main/java/io/github/zyrouge/symphony/ui/view/GenreView.kt index 4fcab52e..29d5778f 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Genre.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/GenreView.kt @@ -21,6 +21,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.zyrouge.symphony.ui.components.AnimatedNowPlayingBottomBar import io.github.zyrouge.symphony.ui.components.GenericSongListDropdown import io.github.zyrouge.symphony.ui.components.IconTextBody diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Home.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/HomeView.kt similarity index 95% rename from app/src/main/java/io/github/zyrouge/symphony/ui/view/Home.kt rename to app/src/main/java/io/github/zyrouge/symphony/ui/view/HomeView.kt index 0420a540..581f019e 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Home.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/HomeView.kt @@ -71,6 +71,7 @@ import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.zyrouge.symphony.services.groove.Groove import io.github.zyrouge.symphony.ui.components.IntroductoryDialog import io.github.zyrouge.symphony.ui.components.NowPlayingBottomBar @@ -79,16 +80,16 @@ import io.github.zyrouge.symphony.ui.components.swipeable import io.github.zyrouge.symphony.ui.helpers.ScaleTransition import io.github.zyrouge.symphony.ui.helpers.SlideTransition import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.ui.view.home.AlbumArtistsView -import io.github.zyrouge.symphony.ui.view.home.AlbumsView -import io.github.zyrouge.symphony.ui.view.home.ArtistsView import io.github.zyrouge.symphony.ui.view.home.BrowserView import io.github.zyrouge.symphony.ui.view.home.FoldersView import io.github.zyrouge.symphony.ui.view.home.ForYouView import io.github.zyrouge.symphony.ui.view.home.GenresView -import io.github.zyrouge.symphony.ui.view.home.PlaylistsView -import io.github.zyrouge.symphony.ui.view.home.SongsView -import io.github.zyrouge.symphony.ui.view.home.TreeView +import io.github.zyrouge.symphony.ui.view.home.HomeAlbumArtistsView +import io.github.zyrouge.symphony.ui.view.home.HomeAlbumsView +import io.github.zyrouge.symphony.ui.view.home.HomeArtistsView +import io.github.zyrouge.symphony.ui.view.home.HomePlaylistsView +import io.github.zyrouge.symphony.ui.view.home.HomeSongsView +import io.github.zyrouge.symphony.ui.view.home.HomeTreeView import kotlinx.serialization.Serializable enum class HomePage( @@ -266,15 +267,15 @@ fun HomeView(context: ViewContext) { ) { page -> when (page) { HomePage.ForYou -> ForYouView(context) - HomePage.Songs -> SongsView(context) - HomePage.Albums -> AlbumsView(context) - HomePage.Artists -> ArtistsView(context) - HomePage.AlbumArtists -> AlbumArtistsView(context) + HomePage.Songs -> HomeSongsView(context) + HomePage.Albums -> HomeAlbumsView(context) + HomePage.Artists -> HomeArtistsView(context) + HomePage.AlbumArtists -> HomeAlbumArtistsView(context) HomePage.Genres -> GenresView(context) HomePage.Browser -> BrowserView(context) HomePage.Folders -> FoldersView(context) - HomePage.Playlists -> PlaylistsView(context) - HomePage.Tree -> TreeView(context) + HomePage.Playlists -> HomePlaylistsView(context) + HomePage.Tree -> HomeTreeView(context) } } }, diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Lyrics.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/LyricsView.kt similarity index 96% rename from app/src/main/java/io/github/zyrouge/symphony/ui/view/Lyrics.kt rename to app/src/main/java/io/github/zyrouge/symphony/ui/view/LyricsView.kt index a7c4917f..c1d2493c 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Lyrics.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/LyricsView.kt @@ -25,13 +25,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.zyrouge.symphony.ui.components.IconButtonPlaceholder import io.github.zyrouge.symphony.ui.components.KeepScreenAwake import io.github.zyrouge.symphony.ui.components.LyricsText import io.github.zyrouge.symphony.ui.components.TimedContentTextStyle import io.github.zyrouge.symphony.ui.components.TopAppBarMinimalTitle import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.ui.view.nowPlaying.NothingPlaying +import io.github.zyrouge.symphony.ui.view.nowPlaying.NowPlayingNothingPlaying import io.github.zyrouge.symphony.ui.view.nowPlaying.NowPlayingSeekBar import io.github.zyrouge.symphony.ui.view.nowPlaying.NowPlayingTraditionalControls import io.github.zyrouge.symphony.ui.view.nowPlaying.defaultHorizontalPadding @@ -117,7 +118,7 @@ fun LyricsView(context: ViewContext) { Spacer(modifier = Modifier.height(defaultHorizontalPadding + 8.dp)) } - else -> NothingPlaying(context) + else -> NowPlayingNothingPlaying(context) } } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/NowPlaying.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/NowPlayingView.kt similarity index 93% rename from app/src/main/java/io/github/zyrouge/symphony/ui/view/NowPlaying.kt rename to app/src/main/java/io/github/zyrouge/symphony/ui/view/NowPlayingView.kt index 3e92591c..304b117e 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/NowPlaying.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/NowPlayingView.kt @@ -5,10 +5,11 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.zyrouge.symphony.services.groove.entities.Song import io.github.zyrouge.symphony.services.radio.RadioQueue import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.ui.view.nowPlaying.NothingPlaying +import io.github.zyrouge.symphony.ui.view.nowPlaying.NowPlayingNothingPlaying import io.github.zyrouge.symphony.ui.view.nowPlaying.NowPlayingBody import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.serialization.Serializable @@ -62,7 +63,7 @@ fun NowPlayingView(context: ViewContext) { NowPlayingObserver(context) { data -> when { data != null -> NowPlayingBody(context, data = data) - else -> NothingPlaying(context) + else -> NowPlayingNothingPlaying(context) } } } @@ -73,10 +74,9 @@ fun NowPlayingObserver( content: @Composable (NowPlayingData?) -> Unit, ) { val queue by context.symphony.radio.observatory.queue.collectAsStateWithLifecycle() - val queueIndex by context.symphony.radio.observatory.queueIndex.collectAsStateWithLifecycle() - val song by remember(queue, queueIndex) { + val song by remember { derivedStateOf { - queue.getOrNull(queueIndex)?.let { context.symphony.groove.song.get(it) } + queue.firstOrNull { it. } } } val isViable by remember(song) { diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Playlist.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/PlaylistView.kt similarity index 100% rename from app/src/main/java/io/github/zyrouge/symphony/ui/view/Playlist.kt rename to app/src/main/java/io/github/zyrouge/symphony/ui/view/PlaylistView.kt diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Queue.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/QueueView.kt similarity index 98% rename from app/src/main/java/io/github/zyrouge/symphony/ui/view/Queue.kt rename to app/src/main/java/io/github/zyrouge/symphony/ui/view/QueueView.kt index 4f948eab..fd2c38db 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Queue.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/QueueView.kt @@ -41,7 +41,7 @@ import io.github.zyrouge.symphony.ui.components.NewPlaylistDialog import io.github.zyrouge.symphony.ui.components.SongCard import io.github.zyrouge.symphony.ui.components.TopAppBarMinimalTitle import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.ui.view.nowPlaying.NothingPlayingBody +import io.github.zyrouge.symphony.ui.view.nowPlaying.NowPlayingNothingPlayingBody import kotlinx.coroutines.launch import kotlinx.serialization.Serializable @@ -125,7 +125,7 @@ fun QueueView(context: ViewContext) { .fillMaxSize() ) { if (queue.isEmpty()) { - NothingPlayingBody(context) + NowPlayingNothingPlayingBody(context) } else { LazyColumn(state = listState) { itemsIndexed( diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Search.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/SearchView.kt similarity index 99% rename from app/src/main/java/io/github/zyrouge/symphony/ui/view/Search.kt rename to app/src/main/java/io/github/zyrouge/symphony/ui/view/SearchView.kt index 6cc50cda..9ab601af 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Search.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/SearchView.kt @@ -63,7 +63,7 @@ import io.github.zyrouge.symphony.ui.components.IconTextBody import io.github.zyrouge.symphony.ui.components.PlaylistDropdownMenu import io.github.zyrouge.symphony.ui.components.SongCard import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.utils.joinToStringIfNotEmpty +import io.github.zyrouge.symphony.utils.builtin.joinToStringIfNotEmpty import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Settings.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/SettingsView.kt similarity index 98% rename from app/src/main/java/io/github/zyrouge/symphony/ui/view/Settings.kt rename to app/src/main/java/io/github/zyrouge/symphony/ui/view/SettingsView.kt index fe9a4928..3505cb14 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Settings.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/SettingsView.kt @@ -59,7 +59,7 @@ import io.github.zyrouge.symphony.ui.view.settings.MiniPlayerSettingsViewRoute import io.github.zyrouge.symphony.ui.view.settings.NowPlayingSettingsViewRoute import io.github.zyrouge.symphony.ui.view.settings.PlayerSettingsViewRoute import io.github.zyrouge.symphony.ui.view.settings.UpdateSettingsViewRoute -import io.github.zyrouge.symphony.utils.ActivityUtils +import io.github.zyrouge.symphony.utils.ActivityHelper import kotlinx.serialization.Serializable @Serializable @@ -275,7 +275,7 @@ private fun LinkChip(context: ViewContext, icon: ImageVector? = null, label: Str ) .clip(RoundedCornerShape(4.dp)) .clickable { - ActivityUtils.startBrowserActivity(context.activity, Uri.parse(url)) + ActivityHelper.startBrowserActivity(context.activity, Uri.parse(url)) } .padding(6.dp, 4.dp), ) { diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Browser.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Browser.kt index 217987a0..0b07108a 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Browser.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Browser.kt @@ -2,6 +2,7 @@ package io.github.zyrouge.symphony.ui.view.home import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.zyrouge.symphony.ui.components.LoaderScaffold import io.github.zyrouge.symphony.ui.helpers.ViewContext diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Folders.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Folders.kt index dfcc92b7..2488ffb7 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Folders.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Folders.kt @@ -2,7 +2,7 @@ package io.github.zyrouge.symphony.ui.view.home import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import io.github.zyrouge.symphony.services.database.store.valuesAsFlow +import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.zyrouge.symphony.ui.components.LoaderScaffold import io.github.zyrouge.symphony.ui.helpers.ViewContext diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/ForYou.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/ForYou.kt index 7e27fe84..605ecea5 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/ForYou.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/ForYou.kt @@ -54,9 +54,9 @@ import io.github.zyrouge.symphony.ui.helpers.ViewContext import io.github.zyrouge.symphony.ui.view.AlbumArtistViewRoute import io.github.zyrouge.symphony.ui.view.AlbumViewRoute import io.github.zyrouge.symphony.ui.view.ArtistViewRoute -import io.github.zyrouge.symphony.utils.randomSubList -import io.github.zyrouge.symphony.utils.runIfOrDefault -import io.github.zyrouge.symphony.utils.subListNonStrict +import io.github.zyrouge.symphony.utils.builtin.randomSubList +import io.github.zyrouge.symphony.utils.builtin.runIfOrDefault +import io.github.zyrouge.symphony.utils.builtin.subListNonStrict enum class ForYou(val label: (context: ViewContext) -> String) { Albums(label = { it.symphony.t.SuggestedAlbums }), diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/AlbumArtists.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/HomeAlbumArtistsView.kt similarity index 91% rename from app/src/main/java/io/github/zyrouge/symphony/ui/view/home/AlbumArtists.kt rename to app/src/main/java/io/github/zyrouge/symphony/ui/view/home/HomeAlbumArtistsView.kt index 6ec05645..052f9c64 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/AlbumArtists.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/HomeAlbumArtistsView.kt @@ -2,6 +2,7 @@ package io.github.zyrouge.symphony.ui.view.home import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.zyrouge.symphony.ui.components.AlbumArtistGrid import io.github.zyrouge.symphony.ui.components.LoaderScaffold import io.github.zyrouge.symphony.ui.helpers.ViewContext @@ -10,7 +11,7 @@ import kotlinx.coroutines.flow.mapLatest @OptIn(ExperimentalCoroutinesApi::class) @Composable -fun AlbumArtistsView(context: ViewContext) { +fun HomeAlbumArtistsView(context: ViewContext) { val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsStateWithLifecycle() val sortBy by context.symphony.settings.lastUsedAlbumArtistsSortBy.flow.collectAsStateWithLifecycle() val sortReverse by context.symphony.settings.lastUsedAlbumArtistsSortReverse.flow.collectAsStateWithLifecycle() diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Albums.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/HomeAlbumsView.kt similarity index 91% rename from app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Albums.kt rename to app/src/main/java/io/github/zyrouge/symphony/ui/view/home/HomeAlbumsView.kt index 38a2f1db..49971da3 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Albums.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/HomeAlbumsView.kt @@ -2,6 +2,7 @@ package io.github.zyrouge.symphony.ui.view.home import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.zyrouge.symphony.ui.components.AlbumGrid import io.github.zyrouge.symphony.ui.components.LoaderScaffold import io.github.zyrouge.symphony.ui.helpers.ViewContext @@ -10,7 +11,7 @@ import kotlinx.coroutines.flow.mapLatest @OptIn(ExperimentalCoroutinesApi::class) @Composable -fun AlbumsView(context: ViewContext) { +fun HomeAlbumsView(context: ViewContext) { val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsStateWithLifecycle() val sortBy by context.symphony.settings.lastUsedAlbumsSortBy.flow.collectAsStateWithLifecycle() val sortReverse by context.symphony.settings.lastUsedAlbumsSortReverse.flow.collectAsStateWithLifecycle() diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Artists.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/HomeArtistsView.kt similarity index 91% rename from app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Artists.kt rename to app/src/main/java/io/github/zyrouge/symphony/ui/view/home/HomeArtistsView.kt index d710a57c..ab423232 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Artists.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/HomeArtistsView.kt @@ -2,6 +2,7 @@ package io.github.zyrouge.symphony.ui.view.home import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.zyrouge.symphony.ui.components.ArtistGrid import io.github.zyrouge.symphony.ui.components.LoaderScaffold import io.github.zyrouge.symphony.ui.helpers.ViewContext @@ -10,7 +11,7 @@ import kotlinx.coroutines.flow.mapLatest @OptIn(ExperimentalCoroutinesApi::class) @Composable -fun ArtistsView(context: ViewContext) { +fun HomeArtistsView(context: ViewContext) { val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsStateWithLifecycle() val sortBy by context.symphony.settings.lastUsedArtistsSortBy.flow.collectAsStateWithLifecycle() val sortReverse by context.symphony.settings.lastUsedArtistsSortReverse.flow.collectAsStateWithLifecycle() diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Playlists.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/HomePlaylistsView.kt similarity index 95% rename from app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Playlists.kt rename to app/src/main/java/io/github/zyrouge/symphony/ui/view/home/HomePlaylistsView.kt index 160eaac6..3c4a1218 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Playlists.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/HomePlaylistsView.kt @@ -24,20 +24,21 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.zyrouge.symphony.services.groove.entities.Playlist import io.github.zyrouge.symphony.services.groove.repositories.PlaylistRepository import io.github.zyrouge.symphony.ui.components.LoaderScaffold import io.github.zyrouge.symphony.ui.components.NewPlaylistDialog import io.github.zyrouge.symphony.ui.components.PlaylistGrid import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.utils.ActivityUtils +import io.github.zyrouge.symphony.utils.ActivityHelper import io.github.zyrouge.symphony.utils.Logger import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.mapLatest @OptIn(ExperimentalCoroutinesApi::class) @Composable -fun PlaylistsView(context: ViewContext) { +fun HomePlaylistsView(context: ViewContext) { val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsStateWithLifecycle() val sortBy by context.symphony.settings.lastUsedPlaylistsSortBy.flow.collectAsStateWithLifecycle() val sortReverse by context.symphony.settings.lastUsedPlaylistsSortReverse.flow.collectAsStateWithLifecycle() @@ -51,7 +52,7 @@ fun PlaylistsView(context: ViewContext) { ) { uris -> uris.forEach { x -> try { - ActivityUtils.makePersistableReadableUri(context.symphony.applicationContext, x) + ActivityHelper.makePersistableReadableUri(context.symphony.applicationContext, x) val id = context.symphony.database.playlistsIdGenerator.next() val parsed = Playlist.parse(context.symphony, id, x) val addOptions = PlaylistRepository.AddOptions( diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Songs.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/HomeSongsView.kt similarity index 91% rename from app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Songs.kt rename to app/src/main/java/io/github/zyrouge/symphony/ui/view/home/HomeSongsView.kt index 4d1b09ce..6d1e9520 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Songs.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/HomeSongsView.kt @@ -2,6 +2,7 @@ package io.github.zyrouge.symphony.ui.view.home import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.zyrouge.symphony.ui.components.LoaderScaffold import io.github.zyrouge.symphony.ui.components.SongList import io.github.zyrouge.symphony.ui.helpers.ViewContext @@ -9,7 +10,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi @OptIn(ExperimentalCoroutinesApi::class) @Composable -fun SongsView(context: ViewContext) { +fun HomeSongsView(context: ViewContext) { val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsStateWithLifecycle() val sortBy by context.symphony.settings.lastUsedSongsSortBy.flow.collectAsStateWithLifecycle() val sortReverse by context.symphony.settings.lastUsedSongsSortReverse.flow.collectAsStateWithLifecycle() diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Tree.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/HomeTreeView.kt similarity index 95% rename from app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Tree.kt rename to app/src/main/java/io/github/zyrouge/symphony/ui/view/home/HomeTreeView.kt index 1f36254b..f64f3616 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Tree.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/HomeTreeView.kt @@ -6,7 +6,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.zyrouge.symphony.ui.helpers.ViewContext @Composable -fun TreeView(context: ViewContext) { +fun HomeTreeView(context: ViewContext) { val isUpdating by context.symphony.groove.exposer.isUpdating.collectAsStateWithLifecycle() // val disabledTreePaths by context.symphony.settings.lastDisabledTreePaths.flow.collectAsStateWithLifecycle() diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/AppBar.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingAppBar.kt similarity index 100% rename from app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/AppBar.kt rename to app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingAppBar.kt diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/Body.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingBody.kt similarity index 100% rename from app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/Body.kt rename to app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingBody.kt diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BottomBar.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingBodyBottomBar.kt similarity index 100% rename from app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BottomBar.kt rename to app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingBodyBottomBar.kt diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BodyContent.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingBodyContent.kt similarity index 99% rename from app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BodyContent.kt rename to app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingBodyContent.kt index f7661ad2..be1b0d0a 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BodyContent.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingBodyContent.kt @@ -56,7 +56,7 @@ import io.github.zyrouge.symphony.ui.helpers.ViewContext import io.github.zyrouge.symphony.ui.view.ArtistViewRoute import io.github.zyrouge.symphony.ui.view.NowPlayingControlsLayout import io.github.zyrouge.symphony.ui.view.NowPlayingData -import io.github.zyrouge.symphony.utils.DurationUtils +import io.github.zyrouge.symphony.utils.DurationHelper @OptIn(ExperimentalLayoutApi::class) @Composable @@ -434,7 +434,7 @@ private fun NowPlayingPlaybackPositionText( alignment: Alignment, ) { val textStyle = MaterialTheme.typography.labelMedium - val durationFormatted = DurationUtils.formatMs(duration) + val durationFormatted = DurationHelper.formatMs(duration) Box(contentAlignment = alignment) { Text( diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BodyCover.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingBodyCover.kt similarity index 100% rename from app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BodyCover.kt rename to app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingBodyCover.kt diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NothingPlaying.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingNothingPlaying.kt similarity index 88% rename from app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NothingPlaying.kt rename to app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingNothingPlaying.kt index 85824141..28df46ae 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NothingPlaying.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingNothingPlaying.kt @@ -14,7 +14,7 @@ import io.github.zyrouge.symphony.ui.components.IconTextBody import io.github.zyrouge.symphony.ui.helpers.ViewContext @Composable -fun NothingPlaying(context: ViewContext) { +fun NowPlayingNothingPlaying(context: ViewContext) { Scaffold( modifier = Modifier.fillMaxSize(), topBar = { @@ -26,14 +26,14 @@ fun NothingPlaying(context: ViewContext) { .padding(contentPadding) .fillMaxSize() ) { - NothingPlayingBody(context) + NowPlayingNothingPlayingBody(context) } } ) } @Composable -fun NothingPlayingBody(context: ViewContext) { +fun NowPlayingNothingPlayingBody(context: ViewContext) { IconTextBody( icon = { modifier -> Icon( diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/PitchDialog.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingPitchDialog.kt similarity index 100% rename from app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/PitchDialog.kt rename to app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingPitchDialog.kt diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/SleepTimerDialog.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingSleepTimerDialog.kt similarity index 98% rename from app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/SleepTimerDialog.kt rename to app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingSleepTimerDialog.kt index f9264da5..7a947de1 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/SleepTimerDialog.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingSleepTimerDialog.kt @@ -39,7 +39,7 @@ import androidx.compose.ui.unit.dp import io.github.zyrouge.symphony.services.radio.Radio import io.github.zyrouge.symphony.ui.components.ScaffoldDialog import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.utils.DurationUtils +import io.github.zyrouge.symphony.utils.DurationHelper import java.time.Duration import java.util.Timer import kotlin.concurrent.timer @@ -75,7 +75,7 @@ fun NowPlayingSleepTimerDialog( }, content = { Text( - DurationUtils.formatMs(endsIn), + DurationHelper.formatMs(endsIn), textAlign = TextAlign.Center, style = MaterialTheme.typography.headlineMedium, modifier = Modifier @@ -167,7 +167,7 @@ fun NowPlayingSleepTimerSetDialog( val shape = RoundedCornerShape(4.dp) Text( - DurationUtils.formatMinSec(0L, 0L, hours, minutes), + DurationHelper.formatMinSec(0L, 0L, hours, minutes), style = MaterialTheme.typography.labelMedium, modifier = Modifier .background( diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/SpeedDialog.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingSpeedDialog.kt similarity index 100% rename from app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/SpeedDialog.kt rename to app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingSpeedDialog.kt diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/AppearanceSettingsView.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsAppearanceView.kt similarity index 99% rename from app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/AppearanceSettingsView.kt rename to app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsAppearanceView.kt index 19bb31f5..a180ca3b 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/AppearanceSettingsView.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsAppearanceView.kt @@ -27,6 +27,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.zyrouge.symphony.services.i18n.CommonTranslation import io.github.zyrouge.symphony.ui.components.IconButtonPlaceholder import io.github.zyrouge.symphony.ui.components.TopAppBarMinimalTitle @@ -53,7 +54,7 @@ object AppearanceSettingsViewRoute @OptIn(ExperimentalMaterial3Api::class) @Composable -fun AppearanceSettingsView(context: ViewContext) { +fun SettingsAppearanceView(context: ViewContext) { val scrollState = rememberScrollState() val language by context.symphony.settings.language.flow.collectAsStateWithLifecycle() val fontFamily by context.symphony.settings.fontFamily.flow.collectAsStateWithLifecycle() diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/GrooveSettingsView.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsGrooveView.kt similarity index 99% rename from app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/GrooveSettingsView.kt rename to app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsGrooveView.kt index 6015b4d0..8ca621a1 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/GrooveSettingsView.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsGrooveView.kt @@ -70,7 +70,7 @@ data class GrooveSettingsViewRoute(val initialElement: String? = null) @OptIn(ExperimentalMaterial3Api::class) @Composable -fun GrooveSettingsView(context: ViewContext, route: GrooveSettingsViewRoute) { +fun SettingsGrooveView(context: ViewContext, route: GrooveSettingsViewRoute) { val coroutineScope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } val scrollState = rememberScrollState() @@ -324,7 +324,7 @@ fun ImagePreserver.Quality.label(context: ViewContext) = when (this) { ImagePreserver.Quality.Low -> context.symphony.t.Low ImagePreserver.Quality.Medium -> context.symphony.t.Medium ImagePreserver.Quality.High -> context.symphony.t.High - ImagePreserver.Quality.Loseless -> context.symphony.t.Loseless + ImagePreserver.Quality.Lossless -> context.symphony.t.Loseless } private fun refreshMediaLibrary(symphony: Symphony, clearCache: Boolean = false) { diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/HomePageSettingsView.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsHomePageView.kt similarity index 99% rename from app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/HomePageSettingsView.kt rename to app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsHomePageView.kt index 0690f7cb..09b1234a 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/HomePageSettingsView.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsHomePageView.kt @@ -40,7 +40,7 @@ object HomePageSettingsViewRoute @OptIn(ExperimentalMaterial3Api::class) @Composable -fun HomePageSettingsView(context: ViewContext) { +fun SettingsHomePageView(context: ViewContext) { val scrollState = rememberScrollState() val homeTabs by context.symphony.settings.homeTabs.flow.collectAsStateWithLifecycle() val forYouContents by context.symphony.settings.forYouContents.flow.collectAsStateWithLifecycle() diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/MiniPlayerSettingsView.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsMiniPlayerView.kt similarity index 99% rename from app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/MiniPlayerSettingsView.kt rename to app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsMiniPlayerView.kt index 3c4c6abd..cca792ac 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/MiniPlayerSettingsView.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsMiniPlayerView.kt @@ -36,7 +36,7 @@ object MiniPlayerSettingsViewRoute @OptIn(ExperimentalMaterial3Api::class) @Composable -fun MiniPlayerSettingsView(context: ViewContext) { +fun SettingsMiniPlayerView(context: ViewContext) { val scrollState = rememberScrollState() val miniPlayerTrackControls by context.symphony.settings.miniPlayerTrackControls.flow.collectAsStateWithLifecycle() val miniPlayerSeekControls by context.symphony.settings.miniPlayerSeekControls.flow.collectAsStateWithLifecycle() diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/NowPlayingSettingsView.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsNowPlayingView.kt similarity index 99% rename from app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/NowPlayingSettingsView.kt rename to app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsNowPlayingView.kt index 5f37d613..29bfa5d9 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/NowPlayingSettingsView.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsNowPlayingView.kt @@ -41,7 +41,7 @@ object NowPlayingSettingsViewRoute @OptIn(ExperimentalMaterial3Api::class) @Composable -fun NowPlayingSettingsView(context: ViewContext) { +fun SettingsNowPlayingView(context: ViewContext) { val scrollState = rememberScrollState() val nowPlayingControlsLayout by context.symphony.settings.nowPlayingControlsLayout.flow.collectAsStateWithLifecycle() val nowPlayingAdditionalInfo by context.symphony.settings.nowPlayingAdditionalInfo.flow.collectAsStateWithLifecycle() diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/PlayerSettingsView.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsPlayerView.kt similarity index 99% rename from app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/PlayerSettingsView.kt rename to app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsPlayerView.kt index dda33249..8bb838d8 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/PlayerSettingsView.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsPlayerView.kt @@ -42,7 +42,7 @@ object PlayerSettingsViewRoute @OptIn(ExperimentalMaterial3Api::class) @Composable -fun PlayerSettingsView(context: ViewContext) { +fun SettingsPlayerView(context: ViewContext) { val scrollState = rememberScrollState() val fadePlayback by context.symphony.settings.fadePlayback.flow.collectAsStateWithLifecycle() val fadePlaybackDuration by context.symphony.settings.fadePlaybackDuration.flow.collectAsStateWithLifecycle() diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/UpdateSettingsView.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsUpdateView.kt similarity index 98% rename from app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/UpdateSettingsView.kt rename to app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsUpdateView.kt index 6e175ce6..b9daf853 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/UpdateSettingsView.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsUpdateView.kt @@ -34,7 +34,7 @@ object UpdateSettingsViewRoute @OptIn(ExperimentalMaterial3Api::class) @Composable -fun UpdateSettingsView(context: ViewContext) { +fun SettingsUpdateView(context: ViewContext) { val scrollState = rememberScrollState() val checkForUpdates by context.symphony.settings.checkForUpdates.flow.collectAsStateWithLifecycle() val showUpdateToast by context.symphony.settings.showUpdateToast.flow.collectAsStateWithLifecycle() diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/ActivityUtils.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/ActivityHelper.kt similarity index 98% rename from app/src/main/java/io/github/zyrouge/symphony/utils/ActivityUtils.kt rename to app/src/main/java/io/github/zyrouge/symphony/utils/ActivityHelper.kt index e687a8a2..d5c8a824 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/utils/ActivityUtils.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/utils/ActivityHelper.kt @@ -8,7 +8,7 @@ import android.net.Uri import android.widget.Toast import io.github.zyrouge.symphony.Symphony -object ActivityUtils { +object ActivityHelper { fun startBrowserActivity(activity: Context, uri: Uri) { activity.startActivity(Intent(Intent.ACTION_VIEW).setData(uri)) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/DurationUtils.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/DurationHelper.kt similarity index 96% rename from app/src/main/java/io/github/zyrouge/symphony/utils/DurationUtils.kt rename to app/src/main/java/io/github/zyrouge/symphony/utils/DurationHelper.kt index c237051d..13a4b159 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/utils/DurationUtils.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/utils/DurationHelper.kt @@ -2,7 +2,7 @@ package io.github.zyrouge.symphony.utils import java.util.concurrent.TimeUnit -object DurationUtils { +object DurationHelper { fun formatMs(ms: Long) = formatMinSec( TimeUnit.MILLISECONDS.toDays(ms).floorDiv(TimeUnit.DAYS.toDays(1)), TimeUnit.MILLISECONDS.toHours(ms) % TimeUnit.DAYS.toHours(1), diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/Fuzzy.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/Fuzzy.kt index d193c2ee..0857e4d5 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/utils/Fuzzy.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/utils/Fuzzy.kt @@ -1,5 +1,6 @@ package io.github.zyrouge.symphony.utils +import io.github.zyrouge.symphony.utils.builtin.subListNonStrict import me.xdrop.fuzzywuzzy.FuzzySearch import kotlin.math.max diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/Http.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/Http.kt index bb5a90b0..49beda89 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/utils/Http.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/utils/Http.kt @@ -2,4 +2,4 @@ package io.github.zyrouge.symphony.utils import okhttp3.OkHttpClient -val HttpClient = OkHttpClient.Builder().cache(null).build() +val DefaultHttpClient = OkHttpClient.Builder().cache(null).build() diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/ImagePreserver.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/ImagePreserver.kt index 0970959e..ec4437c1 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/utils/ImagePreserver.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/utils/ImagePreserver.kt @@ -8,7 +8,7 @@ object ImagePreserver { Low(256), Medium(512), High(1024), - Loseless(null), + Lossless(null), } fun resize(bitmap: Bitmap, quality: Quality): Bitmap { diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/RangeUtils.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/RangeCalculator.kt similarity index 94% rename from app/src/main/java/io/github/zyrouge/symphony/utils/RangeUtils.kt rename to app/src/main/java/io/github/zyrouge/symphony/utils/RangeCalculator.kt index 109af33e..f1db40d8 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/utils/RangeUtils.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/utils/RangeCalculator.kt @@ -1,6 +1,6 @@ package io.github.zyrouge.symphony.utils -object RangeUtils { +object RangeCalculator { fun calculateGap(range: ClosedFloatingPointRange) = range.endInclusive - range.start fun calculateRatioFromValue(value: Float, range: ClosedFloatingPointRange) = diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/StringListUtils.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/StringSorter.kt similarity index 93% rename from app/src/main/java/io/github/zyrouge/symphony/utils/StringListUtils.kt rename to app/src/main/java/io/github/zyrouge/symphony/utils/StringSorter.kt index 121f1789..6d4a5b14 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/utils/StringListUtils.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/utils/StringSorter.kt @@ -1,6 +1,6 @@ package io.github.zyrouge.symphony.utils -object StringListUtils { +object StringSorter { enum class SortBy { CUSTOM, NAME, diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/StringUtils.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/StringUtils.kt deleted file mode 100644 index 04b511dc..00000000 --- a/app/src/main/java/io/github/zyrouge/symphony/utils/StringUtils.kt +++ /dev/null @@ -1,3 +0,0 @@ -package io.github.zyrouge.symphony.utils - -fun String.withCase(sensitive: Boolean) = if (!sensitive) lowercase() else this diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/TimedContent.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/TimedContent.kt index b690e85a..08ca3d51 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/utils/TimedContent.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/utils/TimedContent.kt @@ -28,4 +28,4 @@ data class TimedContent(val pairs: List>) { return TimedContent(pairs) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/Float.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/builtin/Float.kt similarity index 54% rename from app/src/main/java/io/github/zyrouge/symphony/utils/Float.kt rename to app/src/main/java/io/github/zyrouge/symphony/utils/builtin/Float.kt index 876ed715..7427223b 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/utils/Float.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/utils/builtin/Float.kt @@ -1,3 +1,3 @@ -package io.github.zyrouge.symphony.utils +package io.github.zyrouge.symphony.utils.builtin fun Float.toSafeFinite() = if (!isFinite()) 0f else this diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/List.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/builtin/List.kt similarity index 94% rename from app/src/main/java/io/github/zyrouge/symphony/utils/List.kt rename to app/src/main/java/io/github/zyrouge/symphony/utils/builtin/List.kt index c4c5b6a2..97b2d43e 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/utils/List.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/utils/builtin/List.kt @@ -1,4 +1,4 @@ -package io.github.zyrouge.symphony.utils +package io.github.zyrouge.symphony.utils.builtin import java.util.concurrent.CopyOnWriteArrayList import kotlin.math.max diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/Run.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/builtin/Run.kt similarity index 82% rename from app/src/main/java/io/github/zyrouge/symphony/utils/Run.kt rename to app/src/main/java/io/github/zyrouge/symphony/utils/builtin/Run.kt index adb6bbaf..4d28fee4 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/utils/Run.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/utils/builtin/Run.kt @@ -1,4 +1,4 @@ -package io.github.zyrouge.symphony.utils +package io.github.zyrouge.symphony.utils.builtin fun runIfOrDefault(value: Boolean, defaultValue: T, fn: () -> T) = when { value -> fn() diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/Set.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/builtin/Set.kt similarity index 92% rename from app/src/main/java/io/github/zyrouge/symphony/utils/Set.kt rename to app/src/main/java/io/github/zyrouge/symphony/utils/builtin/Set.kt index 143e1f6d..12d20f85 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/utils/Set.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/utils/builtin/Set.kt @@ -1,4 +1,4 @@ -package io.github.zyrouge.symphony.utils +package io.github.zyrouge.symphony.utils.builtin import java.util.concurrent.ConcurrentHashMap diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/builtin/String.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/builtin/String.kt new file mode 100644 index 00000000..695c7b84 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/utils/builtin/String.kt @@ -0,0 +1,12 @@ +package io.github.zyrouge.symphony.utils.builtin + +fun String.withCase(sensitive: Boolean) = if (!sensitive) lowercase() else this + +fun String.repeatJoin(count: Int, separator: String) = when (count) { + 0 -> "" + 1 -> this + else -> (this + separator).repeat(count - 1) + this +} + +// SQL Query Placeholders +fun sqlqph(count: Int) = "?".repeatJoin(count, ", ") From 0c067f2ad4e99f9e2b296339749c4b5b8932ae29 Mon Sep 17 00:00:00 2001 From: Zyrouge Date: Fri, 15 Aug 2025 22:34:44 +0530 Subject: [PATCH 09/15] refactor: db linked list operators --- .../store/SongQueueSongMappingStore.kt | 38 +++++- .../symphony/services/radio/RadioQueue.kt | 122 ++++++++++-------- .../ComplexLinkedListAdditionOperator.kt | 33 +++++ .../ComplexLinkedListOperator.kt | 34 +++++ .../ComplexLinkedListRemoveOperator.kt | 68 ++++++++++ 5 files changed, 242 insertions(+), 53 deletions(-) create mode 100644 app/src/main/java/io/github/zyrouge/symphony/utils/complex_linked_list/ComplexLinkedListAdditionOperator.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/utils/complex_linked_list/ComplexLinkedListOperator.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/utils/complex_linked_list/ComplexLinkedListRemoveOperator.kt diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueSongMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueSongMappingStore.kt index 3034ce29..b590243f 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueSongMappingStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueSongMappingStore.kt @@ -49,7 +49,7 @@ abstract class SongQueueSongMappingStore { @RawQuery protected abstract fun findByNextIdRaw(query: SupportSQLiteQuery): Song.AlongSongQueueMapping? - fun findByNextId(queueId: String, nextId: String): Song.AlongSongQueueMapping? { + fun findByNextId(queueId: String, nextId: String?): Song.AlongSongQueueMapping? { val query = "SELECT ${Song.TABLE}.*, " + "${SongQueueSongMapping.TABLE}.* " + "FROM ${SongQueueSongMapping.TABLE} " + @@ -72,9 +72,43 @@ abstract class SongQueueSongMappingStore { return findHeadRaw(SimpleSQLiteQuery(query, args)) } + protected abstract fun entriesByIdsRaw(query: SupportSQLiteQuery): Map< + @MapColumn(SongQueueSongMapping.COLUMN_ID) String, Song.AlongSongQueueMapping> + + fun entriesByIds(queueId: String, songMappingIds: List): Map< + String, Song.AlongSongQueueMapping> { + val query = "SELECT ${Song.TABLE}.*, " + + "${SongQueueSongMapping.TABLE}.* " + + "FROM ${SongQueueSongMapping.TABLE} " + + "WHERE ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_QUEUE_ID} = ? " + + "AND ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_ID} " + + "IN (${sqlqph(songMappingIds.size)}) " + + "LEFT JOIN ${Song.TABLE} ON ${Song.TABLE}.${Song.COLUMN_ID} = ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_SONG_ID} " + + "ORDER BY ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_IS_HEAD} DESC" + val args = arrayOf(queueId, *songMappingIds.toTypedArray()) + return entriesByIdsRaw(SimpleSQLiteQuery(query, args)) + } + + protected abstract fun entriesByNextIdsRaw(query: SupportSQLiteQuery): Map< + @MapColumn(SongQueueSongMapping.COLUMN_NEXT_ID) String, Song.AlongSongQueueMapping> + + fun entriesByNextIds(queueId: String, songMappingIds: List): Map< + String, Song.AlongSongQueueMapping> { + val query = "SELECT ${Song.TABLE}.*, " + + "${SongQueueSongMapping.TABLE}.* " + + "FROM ${SongQueueSongMapping.TABLE} " + + "WHERE ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_QUEUE_ID} = ? " + + "AND ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_NEXT_ID} " + + "IN (${sqlqph(songMappingIds.size)}) " + + "LEFT JOIN ${Song.TABLE} ON ${Song.TABLE}.${Song.COLUMN_ID} = ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_SONG_ID} " + + "ORDER BY ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_IS_HEAD} DESC" + val args = arrayOf(queueId, *songMappingIds.toTypedArray()) + return entriesByNextIdsRaw(SimpleSQLiteQuery(query, args)) + } + @RawQuery(observedEntities = [Song::class, SongQueueSongMapping::class]) protected abstract fun entriesAsFlowRaw(query: SupportSQLiteQuery): Flow< - Map<@MapColumn(SongQueueSongMapping.COLUMN_SONG_ID) String, Song.AlongSongQueueMapping>> + Map<@MapColumn(SongQueueSongMapping.COLUMN_ID) String, Song.AlongSongQueueMapping>> fun entriesAsFlow(queueId: String): Flow> { val query = "SELECT ${Song.TABLE}.*, " + diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioQueue.kt b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioQueue.kt index 171605c5..436d01d5 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioQueue.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioQueue.kt @@ -1,11 +1,54 @@ package io.github.zyrouge.symphony.services.radio import io.github.zyrouge.symphony.Symphony +import io.github.zyrouge.symphony.services.database.store.SongQueueSongMappingStore import io.github.zyrouge.symphony.services.groove.entities.Song import io.github.zyrouge.symphony.services.groove.entities.SongQueue import io.github.zyrouge.symphony.services.groove.entities.SongQueueSongMapping +import io.github.zyrouge.symphony.utils.complex_linked_list.ComplexLinkedListOperator class RadioQueue(private val symphony: Symphony) { + private class SongQueueSongMappingOperatorEntityFunctions : + ComplexLinkedListOperator.EntityFunctions { + override fun getEntityId(entity: SongQueueSongMapping) = entity.id + override fun getEntityNextId(entity: SongQueueSongMapping) = entity.nextId + override fun getEntityIsHead(entity: SongQueueSongMapping) = entity.isHead + + override fun updateEntityNextId(entity: SongQueueSongMapping, nNextId: String?) = + entity.copy(nextId = nNextId) + + override fun updateEntityIsHead(entity: SongQueueSongMapping, nIsHead: Boolean) = + entity.copy(isHead = nIsHead) + } + + private class SongQueueSongMappingOperatorPersistenceFunctions( + private val store: SongQueueSongMappingStore, + private val queueId: String, + ) : + ComplexLinkedListOperator.PersistenceFunctions { + override fun getEntitiesByIds(ids: List) = store.entriesByIds(queueId, ids) + .mapValues { it.value.mapping } + + override fun getEntitiesByNextIds(nextIds: List) = + store.entriesByNextIds(queueId, nextIds) + .mapValues { it.value.mapping } + + override fun getHeadEntity() = store.findHead(queueId)?.mapping + override fun getTailEntity() = store.findByNextId(queueId, null)?.mapping + + override suspend fun insertEntities(entities: List) { + store.insert(*entities.toTypedArray()) + } + + override suspend fun updateEntities(entities: List) { + store.update(*entities.toTypedArray()) + } + + override suspend fun deleteEntities(ids: List) { + store.delete(queueId, ids) + } + } + // val queueFlow = symphony.database.songQueue.findFirstAsFlow() // val queue = AtomicReference(null) @@ -54,50 +97,37 @@ class RadioQueue(private val symphony: Symphony) { ) symphony.database.songQueue.insert(queue) } - var previousSong = previousSongMappingId?.let { - symphony.database.songQueueSongMapping.findById(queueId, it) - } - var nextMappingId = previousSong?.mapping?.nextId - var ogNextMappingId = previousSong?.mapping?.ogNextId - val added = mutableListOf() - var i = 0 - val songIdsCount = songIds.size - for (x in songIds.reversed()) { - val isHead = origQueue == null && i == songIdsCount - 1 - val mapping = SongQueueSongMapping( + val operator = createSongQueueSongMappingOperator(queueId) + operator.add(previousSongMappingId, songIds) { x, isHead, nextId -> + SongQueueSongMapping( id = symphony.database.songQueueSongMappingIdGenerator.next(), queueId = queueId, songId = x, isHead = isHead, - nextId = nextMappingId, - ogNextId = ogNextMappingId, + nextId = nextId, + ogNextId = nextId, ) - added.add(mapping) - nextMappingId = mapping.id - ogNextMappingId = mapping.id - i++ } - symphony.database.songQueueSongMapping.insert(*added.toTypedArray()) afterAdd(options) } suspend fun add( songId: String, - previousSongId: String? = null, + previousSongMappingId: String? = null, options: Radio.PlayOptions = Radio.PlayOptions(), - ) = add(listOf(songId), previousSongId, options) + ) = add(listOf(songId), previousSongMappingId, options) suspend fun add( songs: List, - previousSongId: String? = null, + previousSongMappingId: String? = null, options: Radio.PlayOptions = Radio.PlayOptions(), - ) = add(songs.map { it.id }, previousSongId, options) + ) = add(songs.map { it.id }, previousSongMappingId, options) suspend fun add( song: Song, - previousSongId: String? = null, + previousSongMappingId: String? = null, options: Radio.PlayOptions = Radio.PlayOptions(), - ) = add(listOf(song.id), previousSongId, options) + ) = add(listOf(song.id), previousSongMappingId, options) private fun afterAdd(options: Radio.PlayOptions) { if (!symphony.radio.hasPlayer) { @@ -106,36 +136,18 @@ class RadioQueue(private val symphony: Symphony) { symphony.radio.onUpdate.dispatch(Radio.Events.Queue.Modified) } - fun remove(id: String) { - originalQueue.removeAt(index) - currentQueue.removeAt(index) - symphony.radio.onUpdate.dispatch(Radio.Events.Queue.Modified) - if (currentSongIndex == index) { - symphony.radio.play(Radio.PlayOptions(index = currentSongIndex)) - } else if (index < currentSongIndex) { - currentSongIndex-- + suspend fun remove(songMappingIds: List): Boolean { + val queue = symphony.database.songQueue.findByInternalId(SONG_QUEUE_INTERNAL_ID_DEFAULT) + if (queue == null) { + return false } + val queueId = queue.entity.id + val operator = createSongQueueSongMappingOperator(queueId) + val result = operator.remove(songMappingIds) + return result.deletedKeys.isNotEmpty() } - fun remove(indices: List) { - var deflection = 0 - var currentSongRemoved = false - val sortedIndices = indices.sortedDescending() - for (i in sortedIndices) { - val index = i - deflection - originalQueue.removeAt(index) - currentQueue.removeAt(index) - when { - i < currentSongIndex -> deflection++ - i == currentSongIndex -> currentSongRemoved = true - } - } - currentSongIndex -= deflection - symphony.radio.onUpdate.dispatch(Radio.Events.Queue.Modified) - if (currentSongRemoved) { - symphony.radio.play(Radio.PlayOptions(index = currentSongIndex)) - } - } + suspend fun remove(songMappingId: String) = remove(listOf(songMappingId)) fun setLoopMode(loopMode: LoopMode) { currentLoopMode = loopMode @@ -169,6 +181,14 @@ class RadioQueue(private val symphony: Symphony) { symphony.radio.onUpdate.dispatch(Radio.Events.Queue.Modified) } + private fun createSongQueueSongMappingOperator(queueId: String) = ComplexLinkedListOperator( + SongQueueSongMappingOperatorEntityFunctions(), + SongQueueSongMappingOperatorPersistenceFunctions( + symphony.database.songQueueSongMapping, + queueId + ) + ) + companion object { const val SONG_QUEUE_INTERNAL_ID_DEFAULT = 1 } diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/complex_linked_list/ComplexLinkedListAdditionOperator.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/complex_linked_list/ComplexLinkedListAdditionOperator.kt new file mode 100644 index 00000000..5c4c0606 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/utils/complex_linked_list/ComplexLinkedListAdditionOperator.kt @@ -0,0 +1,33 @@ +package io.github.zyrouge.symphony.utils.complex_linked_list + +typealias ComplexLinkedListAdditionOperatorCreateFn = (value: X, isHead: Boolean, nextId: K?) -> V + +class ComplexLinkedListAdditionOperator( + val helper: ComplexLinkedListOperator, + val insertAtId: K?, + val values: List, + val createFn: ComplexLinkedListAdditionOperatorCreateFn, +) { + data class Result(val addedKeys: List) + + suspend fun operate(): Result { + val headEntity = helper.persistenceFunctions.getHeadEntity() + val tailEntity = insertAtId?.let { helper.persistenceFunctions.getEntity(it) } + ?: helper.persistenceFunctions.getTailEntity() + val addedKeys = mutableListOf() + val added = mutableListOf() + var nextId = tailEntity?.let { helper.entityFunctions.getEntityNextId(it) } + val count = values.size + for (i in (count - 1) downTo 0) { + val value = values[i] + val isHead = i == 0 && headEntity == null + val entity = createFn(value, isHead, nextId) + val id = helper.entityFunctions.getEntityId(entity) + addedKeys.add(id) + added.add(entity) + nextId = id + } + helper.persistenceFunctions.insertEntities(added) + return Result(addedKeys = addedKeys) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/complex_linked_list/ComplexLinkedListOperator.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/complex_linked_list/ComplexLinkedListOperator.kt new file mode 100644 index 00000000..96ab40ac --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/utils/complex_linked_list/ComplexLinkedListOperator.kt @@ -0,0 +1,34 @@ +package io.github.zyrouge.symphony.utils.complex_linked_list + +class ComplexLinkedListOperator( + val entityFunctions: EntityFunctions, + val persistenceFunctions: PersistenceFunctions, +) { + interface EntityFunctions { + fun getEntityId(entity: V): K + fun getEntityNextId(entity: V): K? + fun getEntityIsHead(entity: V): Boolean + fun updateEntityNextId(entity: V, nNextId: K?): V + fun updateEntityIsHead(entity: V, nIsHead: Boolean): V + } + + interface PersistenceFunctions { + fun getEntitiesByIds(ids: List): Map + fun getEntity(id: K) = getEntitiesByIds(listOf(id))[id] + fun getEntitiesByNextIds(nextIds: List): Map + fun getEntityByNextId(nextId: K) = getEntitiesByIds(listOf(nextId))[nextId] + fun getHeadEntity(): V? + fun getTailEntity(): V? + suspend fun insertEntities(entities: List) + suspend fun updateEntities(entities: List) + suspend fun deleteEntities(ids: List) + } + + suspend fun add( + insertAtId: K?, + values: List, + createFn: ComplexLinkedListAdditionOperatorCreateFn, + ) = ComplexLinkedListAdditionOperator(this, insertAtId, values, createFn).operate() + + suspend fun remove(keys: List) = ComplexLinkedListRemoveOperator(this, keys).operate() +} \ No newline at end of file diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/complex_linked_list/ComplexLinkedListRemoveOperator.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/complex_linked_list/ComplexLinkedListRemoveOperator.kt new file mode 100644 index 00000000..4459ffb7 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/utils/complex_linked_list/ComplexLinkedListRemoveOperator.kt @@ -0,0 +1,68 @@ +package io.github.zyrouge.symphony.utils.complex_linked_list + +class ComplexLinkedListRemoveOperator( + val helper: ComplexLinkedListOperator, + val keys: List, +) { + data class Result( + val headChanged: Boolean, + val modifiedKeys: List, + val deletedKeys: List, + ) + + suspend fun operate(): Result { + val entities = helper.persistenceFunctions.getEntitiesByIds(keys).toMutableMap() + val idToPreviousId = mutableMapOf() + for (x in entities.values) { + val id = helper.entityFunctions.getEntityId(x) + val nextId = helper.entityFunctions.getEntityId(x) + idToPreviousId[nextId] = id + } + for (x in helper.persistenceFunctions.getEntitiesByNextIds(keys).values) { + val id = helper.entityFunctions.getEntityId(x) + val nextId = helper.entityFunctions.getEntityId(x) + entities.put(id, x) + idToPreviousId.put(nextId, id) + } + val modified = mutableSetOf() + val deleted = mutableSetOf() + var headChanged = false + for (id in keys) { + val entity = entities[id] ?: continue + val isHead = helper.entityFunctions.getEntityIsHead(entity) + if (isHead) { + val nextId = helper.entityFunctions.getEntityNextId(entity) ?: continue + val nextEntity = entities[nextId] ?: continue + val nNextEntity = helper.entityFunctions.updateEntityIsHead(nextEntity, true) + entities.put(nextId, nNextEntity) + entities.remove(id) + modified.add(nextId) + modified.remove(id) + deleted.add(id) + headChanged = true + continue + } + val previousId = idToPreviousId[id] ?: continue + val previousEntity = entities[previousId] ?: continue + val nextId = helper.entityFunctions.getEntityNextId(entity) + val nPreviousEntity = helper.entityFunctions.updateEntityNextId(previousEntity, nextId) + entities.put(previousId, nPreviousEntity) + entities.remove(id) + modified.add(previousId) + modified.remove(id) + deleted.add(id) + } + if (modified.isNotEmpty()) { + val modifiedEntities = modified.mapNotNull { entities[it] }.toList() + helper.persistenceFunctions.updateEntities(modifiedEntities) + } + if (deleted.isNotEmpty()) { + helper.persistenceFunctions.deleteEntities(deleted.toList()) + } + return Result( + headChanged = headChanged, + modifiedKeys = modified.toList(), + deletedKeys = deleted.toList(), + ) + } +} \ No newline at end of file From 2a4560194a8be4de0516faad2d314730120a0873 Mon Sep 17 00:00:00 2001 From: Zyrouge Date: Sat, 16 Aug 2025 20:29:47 +0530 Subject: [PATCH 10/15] refactor: rewrite lazy linked list --- .../groove/repositories/PlaylistRepository.kt | 5 - .../symphony/services/radio/RadioQueue.kt | 105 +++++++++++------- .../ComplexLinkedListAdditionOperator.kt | 33 ------ .../LazyLinkedListAppendOperator.kt | 50 +++++++++ .../LazyLinkedListOperatorHelper.kt} | 26 +++-- .../LazyLinkedListPrependHeadOperator.kt | 47 ++++++++ .../LazyLinkedListRemoveOperator.kt} | 25 ++--- 7 files changed, 192 insertions(+), 99 deletions(-) delete mode 100644 app/src/main/java/io/github/zyrouge/symphony/utils/complex_linked_list/ComplexLinkedListAdditionOperator.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListAppendOperator.kt rename app/src/main/java/io/github/zyrouge/symphony/utils/{complex_linked_list/ComplexLinkedListOperator.kt => lazy_linked_list/LazyLinkedListOperatorHelper.kt} (54%) create mode 100644 app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListPrependHeadOperator.kt rename app/src/main/java/io/github/zyrouge/symphony/utils/{complex_linked_list/ComplexLinkedListRemoveOperator.kt => lazy_linked_list/LazyLinkedListRemoveOperator.kt} (82%) diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/PlaylistRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/PlaylistRepository.kt index 1cbd0726..a92085d7 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/PlaylistRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/PlaylistRepository.kt @@ -1,11 +1,6 @@ package io.github.zyrouge.symphony.services.groove.repositories import io.github.zyrouge.symphony.Symphony -import io.github.zyrouge.symphony.services.database.store.findByIdAsFlow -import io.github.zyrouge.symphony.services.database.store.findSongIdsByPlaylistInternalIdAsFlow -import io.github.zyrouge.symphony.services.database.store.findTop4SongArtworksAsFlow -import io.github.zyrouge.symphony.services.database.store.valuesAsFlow -import io.github.zyrouge.symphony.services.database.store.valuesMappedAsFlow import io.github.zyrouge.symphony.services.groove.entities.Playlist import io.github.zyrouge.symphony.services.groove.entities.PlaylistSongMapping import kotlinx.coroutines.ExperimentalCoroutinesApi diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioQueue.kt b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioQueue.kt index 436d01d5..6e27c321 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioQueue.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioQueue.kt @@ -5,11 +5,11 @@ import io.github.zyrouge.symphony.services.database.store.SongQueueSongMappingSt import io.github.zyrouge.symphony.services.groove.entities.Song import io.github.zyrouge.symphony.services.groove.entities.SongQueue import io.github.zyrouge.symphony.services.groove.entities.SongQueueSongMapping -import io.github.zyrouge.symphony.utils.complex_linked_list.ComplexLinkedListOperator +import io.github.zyrouge.symphony.utils.lazy_linked_list.LazyLinkedListOperatorHelper class RadioQueue(private val symphony: Symphony) { private class SongQueueSongMappingOperatorEntityFunctions : - ComplexLinkedListOperator.EntityFunctions { + LazyLinkedListOperatorHelper.EntityFunctions { override fun getEntityId(entity: SongQueueSongMapping) = entity.id override fun getEntityNextId(entity: SongQueueSongMapping) = entity.nextId override fun getEntityIsHead(entity: SongQueueSongMapping) = entity.isHead @@ -25,7 +25,7 @@ class RadioQueue(private val symphony: Symphony) { private val store: SongQueueSongMappingStore, private val queueId: String, ) : - ComplexLinkedListOperator.PersistenceFunctions { + LazyLinkedListOperatorHelper.PersistenceFunctions { override fun getEntitiesByIds(ids: List) = store.entriesByIds(queueId, ids) .mapValues { it.value.mapping } @@ -77,7 +77,7 @@ class RadioQueue(private val symphony: Symphony) { suspend fun add( songIds: List, - previousSongMappingId: String? = null, + insertAfterId: String? = null, options: Radio.PlayOptions = Radio.PlayOptions(), ) { val origQueue = symphony.database.songQueue.findByInternalId(SONG_QUEUE_INTERNAL_ID_DEFAULT) @@ -98,7 +98,7 @@ class RadioQueue(private val symphony: Symphony) { symphony.database.songQueue.insert(queue) } val operator = createSongQueueSongMappingOperator(queueId) - operator.add(previousSongMappingId, songIds) { x, isHead, nextId -> + operator.append(insertAfterId, songIds) { x, isHead, nextId -> SongQueueSongMapping( id = symphony.database.songQueueSongMappingIdGenerator.next(), queueId = queueId, @@ -113,27 +113,27 @@ class RadioQueue(private val symphony: Symphony) { suspend fun add( songId: String, - previousSongMappingId: String? = null, + insertAfterId: String? = null, options: Radio.PlayOptions = Radio.PlayOptions(), - ) = add(listOf(songId), previousSongMappingId, options) + ) = add(listOf(songId), insertAfterId, options) suspend fun add( songs: List, - previousSongMappingId: String? = null, + insertAfterId: String? = null, options: Radio.PlayOptions = Radio.PlayOptions(), - ) = add(songs.map { it.id }, previousSongMappingId, options) + ) = add(songs.map { it.id }, insertAfterId, options) suspend fun add( song: Song, - previousSongMappingId: String? = null, + insertAfterId: String? = null, options: Radio.PlayOptions = Radio.PlayOptions(), - ) = add(listOf(song.id), previousSongMappingId, options) + ) = add(listOf(song.id), insertAfterId, options) private fun afterAdd(options: Radio.PlayOptions) { - if (!symphony.radio.hasPlayer) { - symphony.radio.play(options) - } - symphony.radio.onUpdate.dispatch(Radio.Events.Queue.Modified) +// if (!symphony.radio.hasPlayer) { +// symphony.radio.play(options) +// } +// symphony.radio.onUpdate.dispatch(Radio.Events.Queue.Modified) } suspend fun remove(songMappingIds: List): Boolean { @@ -149,39 +149,64 @@ class RadioQueue(private val symphony: Symphony) { suspend fun remove(songMappingId: String) = remove(listOf(songMappingId)) - fun setLoopMode(loopMode: LoopMode) { - currentLoopMode = loopMode + private suspend fun setLoopMode(queue: SongQueue, loopMode: SongQueue.LoopMode) { + val nQueue = queue.copy(loopMode = loopMode) + symphony.database.songQueue.update(nQueue) } - fun toggleLoopMode() { - val next = (currentLoopMode.ordinal + 1) % LoopMode.values.size - setLoopMode(LoopMode.values[next]) + suspend fun setLoopMode(loopMode: SongQueue.LoopMode): Boolean { + val queue = symphony.database.songQueue.findByInternalId(SONG_QUEUE_INTERNAL_ID_DEFAULT) + if (queue == null) { + return false + } + setLoopMode(queue.entity, loopMode) + return true } - fun toggleShuffleMode() = setShuffleMode(!currentShuffleMode) + suspend fun toggleLoopMode(): Boolean { + val queue = symphony.database.songQueue.findByInternalId(SONG_QUEUE_INTERNAL_ID_DEFAULT) + if (queue == null) { + return false + } + val currentLoopMode = queue.entity.loopMode + val nextLoopModeOrdinal = (currentLoopMode.ordinal + 1) % SongQueue.LoopMode.values.size + val nextLoopMode = SongQueue.LoopMode.values[nextLoopModeOrdinal] + setLoopMode(queue.entity, nextLoopMode) + return true + } + + fun toggleShuffleMode(): Boolean { +// val queue = symphony.database.songQueue.findByInternalId(SONG_QUEUE_INTERNAL_ID_DEFAULT) +// if (queue == null) { +// return false +// } +// val nQueue = queue.entity.copy(shuffled = !queue.entity.shuffled) +// symphony.database.songQueue.update(nQueue) + return true + } fun setShuffleMode(to: Boolean) { - currentShuffleMode = to - if (currentQueue.isNotEmpty()) { - val currentSongId = getSongIdAt(currentSongIndex) ?: getSongIdAt(0)!! - currentSongIndex = if (currentShuffleMode) { - val newQueue = originalQueue.toMutableList() - newQueue.removeAt(currentSongIndex) - newQueue.shuffle() - newQueue.add(0, currentSongId) - currentQueue.clear() - currentQueue.addAll(newQueue) - 0 - } else { - currentQueue.clear() - currentQueue.addAll(originalQueue) - originalQueue.indexOfFirst { it == currentSongId } - } - } - symphony.radio.onUpdate.dispatch(Radio.Events.Queue.Modified) +// currentShuffleMode = to +// if (currentQueue.isNotEmpty()) { +// val currentSongId = getSongIdAt(currentSongIndex) ?: getSongIdAt(0)!! +// currentSongIndex = if (currentShuffleMode) { +// val newQueue = originalQueue.toMutableList() +// newQueue.removeAt(currentSongIndex) +// newQueue.shuffle() +// newQueue.add(0, currentSongId) +// currentQueue.clear() +// currentQueue.addAll(newQueue) +// 0 +// } else { +// currentQueue.clear() +// currentQueue.addAll(originalQueue) +// originalQueue.indexOfFirst { it == currentSongId } +// } +// } +// symphony.radio.onUpdate.dispatch(Radio.Events.Queue.Modified) } - private fun createSongQueueSongMappingOperator(queueId: String) = ComplexLinkedListOperator( + private fun createSongQueueSongMappingOperator(queueId: String) = LazyLinkedListOperatorHelper( SongQueueSongMappingOperatorEntityFunctions(), SongQueueSongMappingOperatorPersistenceFunctions( symphony.database.songQueueSongMapping, diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/complex_linked_list/ComplexLinkedListAdditionOperator.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/complex_linked_list/ComplexLinkedListAdditionOperator.kt deleted file mode 100644 index 5c4c0606..00000000 --- a/app/src/main/java/io/github/zyrouge/symphony/utils/complex_linked_list/ComplexLinkedListAdditionOperator.kt +++ /dev/null @@ -1,33 +0,0 @@ -package io.github.zyrouge.symphony.utils.complex_linked_list - -typealias ComplexLinkedListAdditionOperatorCreateFn = (value: X, isHead: Boolean, nextId: K?) -> V - -class ComplexLinkedListAdditionOperator( - val helper: ComplexLinkedListOperator, - val insertAtId: K?, - val values: List, - val createFn: ComplexLinkedListAdditionOperatorCreateFn, -) { - data class Result(val addedKeys: List) - - suspend fun operate(): Result { - val headEntity = helper.persistenceFunctions.getHeadEntity() - val tailEntity = insertAtId?.let { helper.persistenceFunctions.getEntity(it) } - ?: helper.persistenceFunctions.getTailEntity() - val addedKeys = mutableListOf() - val added = mutableListOf() - var nextId = tailEntity?.let { helper.entityFunctions.getEntityNextId(it) } - val count = values.size - for (i in (count - 1) downTo 0) { - val value = values[i] - val isHead = i == 0 && headEntity == null - val entity = createFn(value, isHead, nextId) - val id = helper.entityFunctions.getEntityId(entity) - addedKeys.add(id) - added.add(entity) - nextId = id - } - helper.persistenceFunctions.insertEntities(added) - return Result(addedKeys = addedKeys) - } -} \ No newline at end of file diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListAppendOperator.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListAppendOperator.kt new file mode 100644 index 00000000..f7c60bb8 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListAppendOperator.kt @@ -0,0 +1,50 @@ +package io.github.zyrouge.symphony.utils.lazy_linked_list + +typealias LazyLinkedListInsertAppendOperatorCreateFn = (value: X, isHead: Boolean, nextId: K?) -> V + +class LazyLinkedListInsertAppendOperator( + val helper: LazyLinkedListOperatorHelper, + val insertAfterId: K?, + val values: List, + val createFn: LazyLinkedListInsertAppendOperatorCreateFn, +) { + suspend fun operate(): LazyLinkedListOperatorHelper.Result { + if (values.isEmpty()) { + return LazyLinkedListOperatorHelper.Result() + } + val addedKeys = mutableListOf() + val added = mutableListOf() + val modifiedKeys = mutableListOf() + val modified = mutableListOf() + val insertAfterEntity = insertAfterId?.let { helper.persistenceFunctions.getEntity(it) } + ?: helper.persistenceFunctions.getTailEntity() + val hasHead = insertAfterEntity != null + var nextId = insertAfterEntity?.let { helper.entityFunctions.getEntityNextId(it) } + for (i in (values.size - 1) downTo 0) { + val value = values[i] + val isHead = i == 0 && !hasHead + val entity = createFn(value, isHead, nextId) + val id = helper.entityFunctions.getEntityId(entity) + addedKeys.add(id) + added.add(entity) + nextId = id + } + if (insertAfterEntity != null) { + val insertAfterId = helper.entityFunctions.getEntityId(insertAfterEntity) + val nInsertAfterEntity = + helper.entityFunctions.updateEntityNextId(insertAfterEntity, nextId) + modifiedKeys.add(insertAfterId) + modified.add(nInsertAfterEntity) + } + if (added.isNotEmpty()) { + helper.persistenceFunctions.insertEntities(added) + } + if (modified.isNotEmpty()) { + helper.persistenceFunctions.updateEntities(modified) + } + return LazyLinkedListOperatorHelper.Result( + addedKeys = addedKeys, + modifiedKeys = modifiedKeys + ) + } +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/complex_linked_list/ComplexLinkedListOperator.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListOperatorHelper.kt similarity index 54% rename from app/src/main/java/io/github/zyrouge/symphony/utils/complex_linked_list/ComplexLinkedListOperator.kt rename to app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListOperatorHelper.kt index 96ab40ac..8d9a7266 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/utils/complex_linked_list/ComplexLinkedListOperator.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListOperatorHelper.kt @@ -1,6 +1,6 @@ -package io.github.zyrouge.symphony.utils.complex_linked_list +package io.github.zyrouge.symphony.utils.lazy_linked_list -class ComplexLinkedListOperator( +class LazyLinkedListOperatorHelper( val entityFunctions: EntityFunctions, val persistenceFunctions: PersistenceFunctions, ) { @@ -24,11 +24,23 @@ class ComplexLinkedListOperator( suspend fun deleteEntities(ids: List) } - suspend fun add( - insertAtId: K?, + data class Result( + val headModified: Boolean = false, + val addedKeys: List = emptyList(), + val modifiedKeys: List = emptyList(), + val deletedKeys: List = emptyList(), + ) + + suspend fun prependHead( + values: List, + createFn: LazyLinkedListPrependHeadOperatorCreateFn, + ) = LazyLinkedListPrependHeadOperator(this, values, createFn).operate() + + suspend fun append( + insertAfterId: K?, values: List, - createFn: ComplexLinkedListAdditionOperatorCreateFn, - ) = ComplexLinkedListAdditionOperator(this, insertAtId, values, createFn).operate() + createFn: LazyLinkedListInsertAppendOperatorCreateFn, + ) = LazyLinkedListInsertAppendOperator(this, insertAfterId, values, createFn).operate() - suspend fun remove(keys: List) = ComplexLinkedListRemoveOperator(this, keys).operate() + suspend fun remove(keys: List) = LazyLinkedListRemoveOperator(this, keys).operate() } \ No newline at end of file diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListPrependHeadOperator.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListPrependHeadOperator.kt new file mode 100644 index 00000000..4c8922cf --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListPrependHeadOperator.kt @@ -0,0 +1,47 @@ +package io.github.zyrouge.symphony.utils.lazy_linked_list + +typealias LazyLinkedListPrependHeadOperatorCreateFn = (value: X, isHead: Boolean, nextId: K?) -> V + +class LazyLinkedListPrependHeadOperator( + val helper: LazyLinkedListOperatorHelper, + val values: List, + val createFn: LazyLinkedListPrependHeadOperatorCreateFn, +) { + suspend fun operate(): LazyLinkedListOperatorHelper.Result { + if (values.isEmpty()) { + return LazyLinkedListOperatorHelper.Result() + } + val addedKeys = mutableListOf() + val added = mutableListOf() + val modifiedKeys = mutableListOf() + val modified = mutableListOf() + val headEntity = helper.persistenceFunctions.getHeadEntity() + var nextId = headEntity?.let { helper.entityFunctions.getEntityId(it) } + for (i in (values.size - 1) downTo 0) { + val value = values[i] + val isHead = i == 0 + val entity = createFn(value, isHead, nextId) + val id = helper.entityFunctions.getEntityId(entity) + addedKeys.add(id) + added.add(entity) + nextId = id + } + if (headEntity != null) { + val headEntityId = helper.entityFunctions.getEntityId(headEntity) + val nHeadEntity = helper.entityFunctions.updateEntityIsHead(headEntity, false) + modifiedKeys.add(headEntityId) + modified.add(nHeadEntity) + } + if (added.isNotEmpty()) { + helper.persistenceFunctions.insertEntities(added) + } + if (modified.isNotEmpty()) { + helper.persistenceFunctions.updateEntities(modified) + } + return LazyLinkedListOperatorHelper.Result( + headModified = true, + addedKeys = addedKeys, + modifiedKeys = modifiedKeys + ) + } +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/complex_linked_list/ComplexLinkedListRemoveOperator.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListRemoveOperator.kt similarity index 82% rename from app/src/main/java/io/github/zyrouge/symphony/utils/complex_linked_list/ComplexLinkedListRemoveOperator.kt rename to app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListRemoveOperator.kt index 4459ffb7..e19275eb 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/utils/complex_linked_list/ComplexLinkedListRemoveOperator.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListRemoveOperator.kt @@ -1,16 +1,13 @@ -package io.github.zyrouge.symphony.utils.complex_linked_list +package io.github.zyrouge.symphony.utils.lazy_linked_list -class ComplexLinkedListRemoveOperator( - val helper: ComplexLinkedListOperator, +class LazyLinkedListRemoveOperator( + val helper: LazyLinkedListOperatorHelper, val keys: List, ) { - data class Result( - val headChanged: Boolean, - val modifiedKeys: List, - val deletedKeys: List, - ) - - suspend fun operate(): Result { + suspend fun operate(): LazyLinkedListOperatorHelper.Result { + if (keys.isEmpty()) { + return LazyLinkedListOperatorHelper.Result() + } val entities = helper.persistenceFunctions.getEntitiesByIds(keys).toMutableMap() val idToPreviousId = mutableMapOf() for (x in entities.values) { @@ -26,7 +23,7 @@ class ComplexLinkedListRemoveOperator( } val modified = mutableSetOf() val deleted = mutableSetOf() - var headChanged = false + var headModified = false for (id in keys) { val entity = entities[id] ?: continue val isHead = helper.entityFunctions.getEntityIsHead(entity) @@ -39,7 +36,7 @@ class ComplexLinkedListRemoveOperator( modified.add(nextId) modified.remove(id) deleted.add(id) - headChanged = true + headModified = true continue } val previousId = idToPreviousId[id] ?: continue @@ -59,8 +56,8 @@ class ComplexLinkedListRemoveOperator( if (deleted.isNotEmpty()) { helper.persistenceFunctions.deleteEntities(deleted.toList()) } - return Result( - headChanged = headChanged, + return LazyLinkedListOperatorHelper.Result( + headModified = headModified, modifiedKeys = modified.toList(), deletedKeys = deleted.toList(), ) From 2b72a474506555d6dcf1d942328a25e34a73572e Mon Sep 17 00:00:00 2001 From: Zyrouge Date: Sat, 16 Aug 2025 20:30:23 +0530 Subject: [PATCH 11/15] chore: add exoplayer & remove unused deps --- app/build.gradle.kts | 3 ++- gradle/libs.versions.toml | 17 ++++------------- metaphony/build.gradle.kts | 7 ------- 3 files changed, 6 insertions(+), 21 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 30d14d7b..6dba14f0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -119,7 +119,8 @@ dependencies { implementation(libs.fuzzywuzzy) implementation(libs.kotlinx.serialization.json) implementation(libs.lifecycle.runtime) - implementation(libs.media) + implementation(libs.media3) + implementation(libs.media3.session) implementation(libs.okhttp3) ksp(libs.room.compiler) implementation(libs.room.ktx) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 78399267..ae5c518b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,7 +11,7 @@ fuzzywuzzy = "1.4.0" junit-jupiter = "5.11.3" kotlinx-serialization-json = "1.7.3" lifecycle-runtime = "2.8.7" -media = "1.7.0" +media3 = "1.8.0" okhttp3 = "4.12.0" room = "2.6.1" @@ -20,14 +20,9 @@ kotlin-gradle-plugin = "2.1.0" kotlin-serialization-plugin = "2.1.0" ksp-plugin = "2.1.0-1.0.29" -compile-sdk = "35" +compile-sdk = "36" min-sdk = "28" target-sdk = "34" -junit = "4.13.2" -junit-version = "1.2.1" -espresso-core = "3.6.1" -appcompat = "1.7.0" -material = "1.12.0" [libraries] activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" } @@ -45,16 +40,12 @@ fuzzywuzzy = { group = "me.xdrop", name = "fuzzywuzzy", version.ref = "fuzzywuzz junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter", version.ref = "junit-jupiter" } kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" } lifecycle-runtime = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle-runtime" } -media = { group = "androidx.media", name = "media", version.ref = "media" } +media3 = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "media3" } +media3-session = { group = "androidx.media3", name = "media3-session", version.ref = "media3" } okhttp3 = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp3" } room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } -junit = { group = "junit", name = "junit", version.ref = "junit" } -ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junit-version" } -espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" } -appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } -material = { group = "com.google.android.material", name = "material", version.ref = "material" } [plugins] android-app = { id = "com.android.application", version.ref = "android-gradle-plugin" } diff --git a/metaphony/build.gradle.kts b/metaphony/build.gradle.kts index d16e3fa9..d99e80e1 100644 --- a/metaphony/build.gradle.kts +++ b/metaphony/build.gradle.kts @@ -61,11 +61,4 @@ android { dependencies { implementation(libs.core) - implementation(libs.appcompat) - implementation(libs.material) - - testImplementation(libs.junit) - - androidTestImplementation(libs.ext.junit) - androidTestImplementation(libs.espresso.core) } \ No newline at end of file From c72677acc28a3767e253f47f1568db2538e7f444 Mon Sep 17 00:00:00 2001 From: Zyrouge Date: Sun, 17 Aug 2025 20:47:36 +0530 Subject: [PATCH 12/15] refactor: partial migration from media to media3 --- .../github/zyrouge/symphony/MainActivity.kt | 1 + .../io/github/zyrouge/symphony/Symphony.kt | 19 +- .../github/zyrouge/symphony/SymphonyHooks.kt | 9 + .../store/SongQueueSongMappingStore.kt | 7 + .../symphony/services/groove/Groove.kt | 3 +- .../zyrouge/symphony/services/radio/Radio.kt | 358 +++----------- .../services/radio/RadioArtworkCacher.kt | 43 -- .../symphony/services/radio/RadioEffects.kt | 89 ---- .../symphony/services/radio/RadioFocus.kt | 65 --- .../radio/RadioNativeLibraryService.kt | 14 + .../services/radio/RadioNativeReceiver.kt | 24 +- .../services/radio/RadioNotification.kt | 111 ----- .../radio/RadioNotificationManager.kt | 106 ---- .../radio/RadioNotificationService.kt | 44 -- .../services/radio/RadioObservatory.kt | 107 ---- .../symphony/services/radio/RadioPlayer.kt | 321 ++++++------ .../symphony/services/radio/RadioQueue.kt | 109 ++-- .../symphony/services/radio/RadioSession.kt | 468 +++++++++--------- .../view/nowPlaying/NowPlayingBodyContent.kt | 3 +- .../LazyLinkedListOperatorHelper.kt | 2 +- 20 files changed, 536 insertions(+), 1367 deletions(-) create mode 100644 app/src/main/java/io/github/zyrouge/symphony/SymphonyHooks.kt delete mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioArtworkCacher.kt delete mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioEffects.kt delete mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioFocus.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioNativeLibraryService.kt delete mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioNotification.kt delete mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioNotificationManager.kt delete mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioNotificationService.kt delete mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioObservatory.kt diff --git a/app/src/main/java/io/github/zyrouge/symphony/MainActivity.kt b/app/src/main/java/io/github/zyrouge/symphony/MainActivity.kt index 19a56dbc..2bcf5d9d 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/MainActivity.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/MainActivity.kt @@ -33,6 +33,7 @@ class MainActivity : ComponentActivity() { val symphony: Symphony by viewModels() symphony.permission.handle(this) gSymphony = symphony + Symphony.globalInstance = symphony symphony.emitActivityReady() attachHandlers() diff --git a/app/src/main/java/io/github/zyrouge/symphony/Symphony.kt b/app/src/main/java/io/github/zyrouge/symphony/Symphony.kt index 30733dc9..395ac413 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/Symphony.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/Symphony.kt @@ -1,6 +1,7 @@ package io.github.zyrouge.symphony import android.app.Application +import android.content.Context import android.widget.Toast import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -18,15 +19,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -class Symphony(application: Application) : AndroidViewModel(application), Symphony.Hooks { - interface Hooks { - fun onSymphonyReady() {} - fun onSymphonyDestroy() {} - fun onSymphonyActivityReady() {} - fun onSymphonyActivityPause() {} - fun onSymphonyActivityDestroy() {} - } - +class Symphony(application: Application) : AndroidViewModel(application), SymphonyHooks { val permission = Permissions(this) val settings = Settings(this) val database = Database(this) @@ -36,7 +29,7 @@ class Symphony(application: Application) : AndroidViewModel(application), Sympho var t by mutableStateOf(translator.getCurrentTranslation()) - val applicationContext get() = getApplication().applicationContext + val applicationContext: Context get() = getApplication().applicationContext var closeApp: (() -> Unit)? = null private var isReady = false private var hooks = listOf(this, radio, groove) @@ -80,7 +73,7 @@ class Symphony(application: Application) : AndroidViewModel(application), Sympho emitDestroy() } - private fun notifyHooks(fn: Hooks.() -> Unit) { + private fun notifyHooks(fn: SymphonyHooks.() -> Unit) { hooks.forEach { fn.invoke(it) } } @@ -106,4 +99,8 @@ class Symphony(application: Application) : AndroidViewModel(application), Sympho } } } + + companion object { + var globalInstance: Symphony? = null + } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/SymphonyHooks.kt b/app/src/main/java/io/github/zyrouge/symphony/SymphonyHooks.kt new file mode 100644 index 00000000..8a6fad0c --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/SymphonyHooks.kt @@ -0,0 +1,9 @@ +package io.github.zyrouge.symphony + +interface SymphonyHooks { + fun onSymphonyReady() {} + fun onSymphonyDestroy() {} + fun onSymphonyActivityReady() {} + fun onSymphonyActivityPause() {} + fun onSymphonyActivityDestroy() {} +} \ No newline at end of file diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueSongMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueSongMappingStore.kt index b590243f..f2fe032d 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueSongMappingStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueSongMappingStore.kt @@ -33,6 +33,13 @@ abstract class SongQueueSongMappingStore { return delete(SimpleSQLiteQuery(query, args)) } + suspend fun deleteAll(queueId: String): Int { + val query = "DELETE FROM ${SongQueueSongMapping.TABLE} " + + "WHERE ${SongQueueSongMapping.COLUMN_QUEUE_ID} = ? " + val args = arrayOf(queueId) + return delete(SimpleSQLiteQuery(query, args)) + } + @RawQuery protected abstract fun findById(query: SupportSQLiteQuery): Song.AlongSongQueueMapping? diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/Groove.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/Groove.kt index 5861401f..8d501472 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/Groove.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/Groove.kt @@ -1,6 +1,7 @@ package io.github.zyrouge.symphony.services.groove import io.github.zyrouge.symphony.Symphony +import io.github.zyrouge.symphony.SymphonyHooks import io.github.zyrouge.symphony.services.groove.repositories.AlbumRepository import io.github.zyrouge.symphony.services.groove.repositories.ArtistRepository import io.github.zyrouge.symphony.services.groove.repositories.GenreRepository @@ -11,7 +12,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -class Groove(private val symphony: Symphony) : Symphony.Hooks { +class Groove(private val symphony: Symphony) : SymphonyHooks { enum class Kind { SONG, ALBUM, diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/radio/Radio.kt b/app/src/main/java/io/github/zyrouge/symphony/services/radio/Radio.kt index 42d6004d..f2e2d8b4 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/radio/Radio.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/radio/Radio.kt @@ -1,205 +1,57 @@ package io.github.zyrouge.symphony.services.radio import io.github.zyrouge.symphony.Symphony -import io.github.zyrouge.symphony.services.groove.entities.Song -import io.github.zyrouge.symphony.utils.Logger +import io.github.zyrouge.symphony.SymphonyHooks +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import java.time.Instant import java.util.Date import java.util.Timer -class Radio(private val symphony: Symphony) : Symphony.Hooks { +class Radio(private val symphony: Symphony) : SymphonyHooks { data class SleepTimer( val duration: Long, val endsAt: Long, - val timer: Timer, - var quitOnEnd: Boolean, + val quitOnEnd: Boolean, ) - val queue = RadioQueue(symphony) - val shorty = RadioShorty(symphony) - val session = RadioSession(symphony) + private val queue = RadioQueue(symphony) + private val player = RadioPlayer(symphony) - private val focus = RadioFocus(symphony) - private val nativeReceiver = RadioNativeReceiver(symphony) - private var player: RadioPlayer? = null - private var nextPlayer: RadioPlayer? = null + private var _sleepTimerTimer: Timer? = null + private val _sleepTimer = MutableStateFlow(null) + val sleepTimer = _sleepTimer.asStateFlow() + private val _pauseOnCurrentSongEnd = MutableStateFlow(false) + val pauseOnCurrentSongEnd = _pauseOnCurrentSongEnd.asStateFlow() - init { - nativeReceiver.start() - } - - fun ready() { - session.start() - } - - fun destroy() { - stop() - session.destroy() - nativeReceiver.destroy() - } + val isPlaying get() = player.isPlaying + val playbackPosition get() = player.playbackPosition + val volume get() = player.volume + val speed get() = player.speed + val pitch get() = player.pitch - data class PlayOptions( - val songMappingId: String? = null, - val autostart: Boolean = true, - val startPosition: Long? = null, - ) - - suspend fun play(options: PlayOptions) { - stopCurrentSong() - // TODO: can queue be nullable? - val queue = - symphony.database.songQueue.findByInternalId(RadioQueue.SONG_QUEUE_INTERNAL_ID_DEFAULT) - if (queue == null) { - onSongFinish(SongFinishSource.Exception) - return - } - val song = options.songMappingId - ?.let { symphony.database.songQueueSongMapping.findById(queue.entity.id, it) } - ?: symphony.database.songQueueSongMapping.findHead(queue.entity.id) - if (song == null) { - onSongFinish(SongFinishSource.Exception) - return - } - try { - val nQueue = queue.entity.copy(playingId = song.mapping.id) - symphony.database.songQueue.update(nQueue) - player = nextPlayer?.takeIf { - when { - it.id == song.entity.id -> true - else -> { - it.destroy() - false - } - } - } ?: RadioPlayer(symphony, song.entity.id, song.entity.uri) - nextPlayer = null - player!!.setOnPreparedListener { - options.startPosition?.let { - if (it > 0L) { - seek(it) - } - } - setSpeed(queue.entity.speed, true) - setPitch(queue.entity.pitch, true) - if (options.autostart) { - start() - } - } - player!!.setOnPlaybackPositionListener { - // TODO - // onPlaybackPositionUpdate.dispatch(it) - } - player!!.setOnFinishListener { - onSongFinish(SongFinishSource.Finish) - } - player!!.setOnErrorListener { what, extra -> - Logger.warn( - "Radio", - "skipping song ${song.entity.id} (${song.mapping.id}) due to $what + $extra" - ) - when { - // happens when change playback params fail, we skip it since its non-critical - what == 1 && extra == -22 -> onSongFinish(SongFinishSource.Finish) - else -> { - removeFromQueue(queue.entity.id, song) - onSongFinish(SongFinishSource.Exception) - } - } - } - player!!.prepare() - prepareNextPlayer() - } catch (err: Exception) { - Logger.warn( - "Radio", - "skipping song ${song.entity.id} (${song.mapping.id})", - err, - ) - removeFromQueue(queue.entity.id, song) + suspend fun play(): Boolean { + if (player.hasMedia()) { + player.play() + return true } + val songQueue = queue.getCurrentSongQueue() ?: return false + val song = symphony.database.songQueueSongMapping.findHead(songQueue.entity.id) + ?: return false + player.setMedia(song.entity.uri) + player.play() + return true } - private suspend fun removeFromQueue(queueId: String, song: Song.AlongSongQueueMapping) { - val previousSong = - symphony.database.songQueueSongMapping.findByNextId(queueId, song.mapping.id) - if (previousSong == null) { - // TODO: handle this - return - } - val nPreviousSongMapping = previousSong.mapping.copy( - nextId = song.mapping.nextId, - ogNextId = song.mapping.ogNextId, - ) - symphony.database.songQueueSongMapping.delete(queueId, listOf(song.mapping.id)) + suspend fun pause() { + player.pause() } - private fun prepareNextPlayer() { - if (!symphony.settings.gaplessPlayback.value) { - return - } - val (nextSongIndex) = getNextSong(SongFinishSource.Finish) - val song = queue.getSongIdAt(nextSongIndex)?.let { symphony.groove.song.get(it) } ?: return - if (song.id == nextPlayer?.id) { - return - } - try { - nextPlayer?.destroy() - nextPlayer = RadioPlayer(symphony, song.id, song.uri).also { - it.prepare() - } - } catch (err: Exception) { - Logger.warn( - "Radio", - "unable to prepare next player ${song.id} (${nextSongIndex})", - err, - ) - } - } - - fun resume() = start() - - private fun start() { - player?.let { - val hasFocus = focus.requestFocus() - if (symphony.settings.requireAudioFocus.value && !hasFocus) { - return - } - if (it.fadePlayback) { - it.changeVolumeInstant(RadioPlayer.MIN_VOLUME) - } - it.changeVolume(RadioPlayer.MAX_VOLUME) {} - it.start() - } - } - - fun pause() = pause {} - - private fun pause(forceFade: Boolean = false, onFinish: () -> Unit) { - player?.let { - if (!it.isPlaying) { - return@let - } - it.changeVolume( - to = RadioPlayer.MIN_VOLUME, - forceFade = forceFade, - ) { _ -> - it.pause() - focus.abandonFocus() - onFinish() - } - } - } - - fun pauseInstant() { - player?.pause() - } - - fun stop(ended: Boolean = true) { - stopCurrentSong() - queue.reset() - clearSleepTimer() - persistedSpeed = RadioPlayer.DEFAULT_SPEED - persistedPitch = RadioPlayer.DEFAULT_PITCH - if (ended) onUpdate.dispatch(Events.Player.Ended) + suspend fun stop() { + player.stop() + queue.clear() } fun jumpTo(index: Int) = play(PlayOptions(index = index)) @@ -215,152 +67,62 @@ class Radio(private val symphony: Symphony) : Symphony.Hooks { } } - fun duck() { - player?.let { - it.changeVolume(RadioPlayer.DUCK_VOLUME) {} - } - } - - fun restoreVolume() { - player?.let { - it.changeVolume(RadioPlayer.MAX_VOLUME) {} - } - } - - fun setSpeed(speed: Float, persist: Boolean) { - player?.let { - it.changeSpeed(speed) - if (persist) { - persistedSpeed = speed - } - onUpdate.dispatch(Events.QueueOption.SpeedChanged) - } + suspend fun setSpeed(speed: Float, persist: Boolean) { + // TODO: implement persist + player.setSpeed(speed) } - fun setPitch(pitch: Float, persist: Boolean) { - player?.let { - it.changePitch(pitch) - if (persist) { - persistedPitch = pitch - } - onUpdate.dispatch(Events.QueueOption.PitchChanged) - } + suspend fun setPitch(pitch: Float, persist: Boolean) { + // TODO: implement persist + player.setPitch(pitch) } - fun setSleepTimer( - duration: Long, - quitOnEnd: Boolean, - ) { + fun setSleepTimer(duration: Long, quitOnEnd: Boolean = false) { val endsAt = System.currentTimeMillis() + duration + val sleepTimer = SleepTimer(duration = duration, endsAt = endsAt, quitOnEnd = quitOnEnd) val timer = Timer() timer.schedule( kotlin.concurrent.timerTask { - val shouldQuit = sleepTimer?.quitOnEnd ?: quitOnEnd + val shouldQuit = sleepTimer.quitOnEnd clearSleepTimer() - pause(forceFade = true) { - if (shouldQuit) { - symphony.closeApp?.invoke() - } + symphony.groove.coroutineScope.launch { + pause() + } + if (shouldQuit) { + symphony.closeApp?.invoke() } }, Date.from(Instant.ofEpochMilli(endsAt)), ) clearSleepTimer() - sleepTimer = SleepTimer( - duration = duration, - endsAt = endsAt, - timer = timer, - quitOnEnd = quitOnEnd, - ) - onUpdate.dispatch(Events.QueueOption.SleepTimerChanged) + _sleepTimerTimer = timer + _sleepTimer.update { sleepTimer } } fun clearSleepTimer() { - sleepTimer?.timer?.cancel() - sleepTimer = null - onUpdate.dispatch(Events.QueueOption.SleepTimerChanged) + _sleepTimerTimer?.cancel() + _sleepTimerTimer = null + _sleepTimer.update { null } } - @JvmName("setPauseOnCurrentSongEndTo") fun setPauseOnCurrentSongEnd(value: Boolean) { - pauseOnCurrentSongEnd = value - onUpdate.dispatch(Events.QueueOption.PauseOnCurrentSongEndChanged) - } - - private fun stopCurrentSong() { - player?.let { - player = null - it.setOnPlaybackPositionListener {} - it.changeVolume(RadioPlayer.MIN_VOLUME) { _ -> - it.stop() - onUpdate.dispatch(Events.Player.Stopped) - } - } + _pauseOnCurrentSongEnd.update { value } } - private enum class SongFinishSource { - Finish, - Exception, - } + private fun onCurrentSongEnded() { - private fun onSongFinish(source: SongFinishSource) { - stopCurrentSong() - if (queue.isEmpty()) { - queue.currentSongIndex = -1 - return - } - var (nextSongIndex, autostart) = getNextSong(source) - if (pauseOnCurrentSongEnd) { - autostart = false - setPauseOnCurrentSongEnd(false) - } - play(PlayOptions(nextSongIndex, autostart = autostart)) } - private fun getNextSong(source: SongFinishSource): Pair { - if (queue.isEmpty()) { - return -1 to false - } - var autostart: Boolean - var nextSongIndex: Int - when (queue.currentLoopMode) { - RadioQueue.LoopMode.Song -> { - nextSongIndex = queue.currentSongIndex - autostart = source == SongFinishSource.Finish - if (!queue.hasSongAt(nextSongIndex)) { - nextSongIndex = 0 - autostart = false - } - } + private fun onNextSongChange() { - else -> { - nextSongIndex = when (source) { - SongFinishSource.Finish -> queue.currentSongIndex + 1 - SongFinishSource.Exception -> queue.currentSongIndex - } - autostart = true - if (!queue.hasSongAt(nextSongIndex)) { - nextSongIndex = 0 - autostart = queue.currentLoopMode == RadioQueue.LoopMode.Queue - } - } - } - return nextSongIndex to autostart } - internal fun watchQueueUpdates(event: Events) { - if (event !is Events.Queue) { - return - } - prepareNextPlayer() + private enum class SongEndedReason { + Finish, + Exception, } - override fun onSymphonyReady() { - ready() - } + private fun onSongEnded(source: SongEndedReason) { - override fun onSymphonyDestroy() { - saveCurrentQueue() - destroy() } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioArtworkCacher.kt b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioArtworkCacher.kt deleted file mode 100644 index 59e75ba9..00000000 --- a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioArtworkCacher.kt +++ /dev/null @@ -1,43 +0,0 @@ -package io.github.zyrouge.symphony.services.radio - -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import androidx.core.graphics.drawable.toBitmap -import coil.imageLoader -import io.github.zyrouge.symphony.Symphony -import io.github.zyrouge.symphony.services.groove.Song -import io.github.zyrouge.symphony.ui.helpers.Assets - -class RadioArtworkCacher(val symphony: Symphony) { - private var default: Bitmap? = null - private var cached = mutableMapOf() - private val cacheLimit = 3 - - suspend fun getArtwork(song: Song): Bitmap { - return cached[song.id] ?: kotlin.run { - val result = symphony.applicationContext.imageLoader - .execute(song.createArtworkImageRequest(symphony).build()) - val bitmap = result.drawable?.toBitmap() ?: getDefaultArtwork() - updateCache(song.id, bitmap) - bitmap - } - } - - private fun getDefaultArtwork(): Bitmap { - return default ?: run { - val bitmap = BitmapFactory.decodeResource( - symphony.applicationContext.resources, - Assets.placeholderDarkId, - ) - default = bitmap - bitmap - } - } - - private fun updateCache(key: String, value: Bitmap) { - if (!cached.containsKey(key) && cached.size >= cacheLimit) { - cached.remove(cached.keys.first()) - } - cached[key] = value - } -} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioEffects.kt b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioEffects.kt deleted file mode 100644 index 0b73a9fb..00000000 --- a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioEffects.kt +++ /dev/null @@ -1,89 +0,0 @@ -package io.github.zyrouge.symphony.services.radio - -import java.util.Timer -import kotlin.math.max -import kotlin.math.min - -object RadioEffects { - class Fader( - val options: Options, - val onUpdate: (Float) -> Unit, - val onFinish: (Boolean) -> Unit, - ) { - data class Options( - val from: Float, - val to: Float, - val duration: Int, - val interval: Int = DEFAULT_INTERVAL, - ) { - companion object { - private const val DEFAULT_INTERVAL = 50 - } - } - - private var timer: Timer? = null - private var ended = false - - fun start() { - val increments = - (options.to - options.from) * (options.interval.toFloat() / options.duration) - var volume = options.from - val isReverse = options.to < options.from - timer = kotlin.concurrent.timer(period = options.interval.toLong()) { - if (volume != options.to) { - onUpdate(volume) - volume = when { - isReverse -> max(options.to, volume + increments) - else -> min(options.to, volume + increments) - } - } else { - ended = true - onFinish(true) - destroy() - } - } - } - - fun stop() { - if (!ended) onFinish(false) - destroy() - } - - private fun destroy() { - timer?.cancel() - timer = null - } - } - -// fun fadeIn(player: RadioPlayer, onEnd: () -> Unit) { -// val options = Fader.Options( -// when { -// player.isPlaying -> player.volume -// else -> RadioPlayer.MIN_VOLUME -// }, -// RadioPlayer.MAX_VOLUME, -// ) -// val fader = Fader( -// options, -// onUpdate = { player.setVolume(it) }, -// onFinish = { onEnd() } -// ) -// player.setVolume(options.from) -// player.start() -// fader.start() -// } -// -// fun fadeOut(player: RadioPlayer, onEnd: () -> Unit) { -// val options = Fader.Options(player.volume, RadioPlayer.MIN_VOLUME) -// val fader = Fader( -// options, -// onUpdate = { player.setVolume(it) }, -// onFinish = { -// player.pause() -// onEnd() -// } -// ) -// player.setVolume(options.from) -// fader.start() -// } -} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioFocus.kt b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioFocus.kt deleted file mode 100644 index 7043c456..00000000 --- a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioFocus.kt +++ /dev/null @@ -1,65 +0,0 @@ -package io.github.zyrouge.symphony.services.radio - -import android.media.AudioManager -import androidx.media.AudioAttributesCompat -import androidx.media.AudioFocusRequestCompat -import androidx.media.AudioManagerCompat -import io.github.zyrouge.symphony.Symphony - -// Credits: https://github.com/RetroMusicPlayer/RetroMusicPlayer/blob/7b1593009319c8d8e04660470ba37f814e8203eb/app/src/main/java/code/name/monkey/retromusic/service/LocalPlayback.kt -class RadioFocus(val symphony: Symphony) { - var hasFocus = false - private set - private var restoreVolumeOnFocusGain = false - - private val audioManager: AudioManager = - symphony.applicationContext.getSystemService(AudioManager::class.java) - - private val audioFocusRequest: AudioFocusRequestCompat = - AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN) - .setAudioAttributes( - AudioAttributesCompat.Builder() - .setContentType(AudioAttributesCompat.CONTENT_TYPE_MUSIC) - .build() - ) - .setOnAudioFocusChangeListener { event -> - when (event) { - AudioManager.AUDIOFOCUS_GAIN -> { - hasFocus = true - if (restoreVolumeOnFocusGain) { - restoreVolumeOnFocusGain = false - when { - symphony.radio.isPlaying -> symphony.radio.restoreVolume() - else -> symphony.radio.resume() - } - } - } - - AudioManager.AUDIOFOCUS_LOSS, AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> { - hasFocus = false - restoreVolumeOnFocusGain = symphony.radio.isPlaying - if (!symphony.settings.ignoreAudioFocusLoss.value) { - symphony.radio.pause() - } - } - - AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> { - restoreVolumeOnFocusGain = symphony.radio.isPlaying - if (symphony.radio.isPlaying) { - symphony.radio.duck() - } - } - } - } - .build() - - fun requestFocus() = AudioManagerCompat.requestAudioFocus( - audioManager, - audioFocusRequest - ) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED - - fun abandonFocus() = AudioManagerCompat.abandonAudioFocusRequest( - audioManager, - audioFocusRequest - ) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED -} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioNativeLibraryService.kt b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioNativeLibraryService.kt new file mode 100644 index 00000000..7801614a --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioNativeLibraryService.kt @@ -0,0 +1,14 @@ +package io.github.zyrouge.symphony.services.radio + +import androidx.media3.session.MediaLibraryService +import androidx.media3.session.MediaSession + +class RadioNativeLibraryService : MediaLibraryService() { + class Callback : MediaLibrarySession.Callback + + override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaLibrarySession? { +// val symphony = Symphony.globalInstance ?: return null +// return MediaLibrarySession.Builder(this, symphony.radio.media, Callback()).build() + return null + } +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioNativeReceiver.kt b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioNativeReceiver.kt index 05bcb10c..00d64df2 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioNativeReceiver.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioNativeReceiver.kt @@ -42,20 +42,20 @@ class RadioNativeReceiver(private val symphony: Symphony) : BroadcastReceiver() } private fun onHeadphonesConnect() { - if (!symphony.radio.hasPlayer) { - return - } - if (!symphony.radio.isPlaying && symphony.settings.playOnHeadphonesConnect.value) { - symphony.radio.resume() - } +// if (!symphony.radio.hasPlayer) { +// return +// } +// if (!symphony.radio.isPlaying && symphony.settings.playOnHeadphonesConnect.value) { +// symphony.radio.resume() +// } } private fun onHeadphonesDisconnect() { - if (!symphony.radio.hasPlayer) { - return - } - if (symphony.radio.isPlaying && symphony.settings.pauseOnHeadphonesDisconnect.value) { - symphony.radio.pauseInstant() - } +// if (!symphony.radio.hasPlayer) { +// return +// } +// if (symphony.radio.isPlaying && symphony.settings.pauseOnHeadphonesDisconnect.value) { +// symphony.radio.pauseInstant() +// } } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioNotification.kt b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioNotification.kt deleted file mode 100644 index 6b987e33..00000000 --- a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioNotification.kt +++ /dev/null @@ -1,111 +0,0 @@ -package io.github.zyrouge.symphony.services.radio - -import android.app.PendingIntent -import android.content.Intent -import androidx.core.app.NotificationCompat -import io.github.zyrouge.symphony.MainActivity -import io.github.zyrouge.symphony.R -import io.github.zyrouge.symphony.Symphony -import io.github.zyrouge.symphony.utils.Logger - - -class RadioNotification(private val symphony: Symphony) { - private var manager = RadioNotificationManager(symphony) - - fun start() { - manager.prepare() - } - - fun cancel() { - manager.cancel() - } - - fun update(req: RadioSession.UpdateRequest) { - val notification = NotificationCompat.Builder( - symphony.applicationContext, - CHANNEL_ID - ).run { - setSmallIcon(R.drawable.material_icon_music_note) - setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - setContentIntent( - PendingIntent.getActivity( - symphony.applicationContext, - 0, - Intent(symphony.applicationContext, MainActivity::class.java) - .addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT), - PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, - ) - ) - setContentTitle(req.song.title) - setContentText(req.song.artists.joinToString(", ")) - setLargeIcon(req.artworkBitmap) - setOngoing(req.isPlaying) - addAction( - createAction( - R.drawable.material_icon_skip_previous, - symphony.t.Previous, - RadioSession.ACTION_PREVIOUS - ) - ) - addAction( - when { - req.isPlaying -> createAction( - R.drawable.material_icon_pause, - symphony.t.Play, - RadioSession.ACTION_PLAY_PAUSE - ) - - else -> createAction( - R.drawable.material_icon_play, - symphony.t.Pause, - RadioSession.ACTION_PLAY_PAUSE - ) - } - ) - addAction( - createAction( - R.drawable.material_icon_skip_next, - symphony.t.Next, - RadioSession.ACTION_NEXT - ) - ) - addAction( - createAction( - R.drawable.material_icon_stop, - symphony.t.Stop, - RadioSession.ACTION_STOP - ) - ) - setStyle( - androidx.media.app.NotificationCompat.MediaStyle() - .setMediaSession(symphony.radio.session.mediaSession.sessionToken) - .setShowActionsInCompactView(0, 1, 2) - ) - } - try { - manager.notify(notification.build()) - } catch (err: Exception) { - Logger.error("RadioNotification", "unable to update notification", err) - } - } - - private fun createAction(icon: Int, title: String, action: String): NotificationCompat.Action { - return NotificationCompat.Action - .Builder(icon, title, createActionIntent(action)) - .build() - } - - private fun createActionIntent(action: String): PendingIntent { - return PendingIntent.getBroadcast( - symphony.applicationContext, - 0, - Intent(action), - PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT - ) - } - - companion object { - val CHANNEL_ID = "${R.string.app_name}_media_notification" - const val NOTIFICATION_ID = 69421 - } -} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioNotificationManager.kt b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioNotificationManager.kt deleted file mode 100644 index ff8aa447..00000000 --- a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioNotificationManager.kt +++ /dev/null @@ -1,106 +0,0 @@ -package io.github.zyrouge.symphony.services.radio - -import android.app.Notification -import android.content.Intent -import android.content.pm.ServiceInfo -import android.os.Build -import androidx.core.app.NotificationChannelCompat -import androidx.core.app.NotificationManagerCompat -import androidx.core.app.ServiceCompat -import io.github.zyrouge.symphony.R -import io.github.zyrouge.symphony.Symphony - -class RadioNotificationManager(val symphony: Symphony) { - private var manager = NotificationManagerCompat.from(symphony.applicationContext) - private var lastNotification: Notification? = null - - enum class State { - PREPARING, - READY, - DESTROYED, - } - - private var state = State.DESTROYED - private val service: RadioNotificationService? - get() = RadioNotificationService.instance - private val hasService: Boolean - get() = state == State.READY && service != null - - fun prepare() { - manager.createNotificationChannel( - NotificationChannelCompat.Builder( - RadioNotification.CHANNEL_ID, - NotificationManagerCompat.IMPORTANCE_LOW, - ).run { - setName(symphony.applicationContext.getString(R.string.app_name)) - setLightsEnabled(false) - setVibrationEnabled(false) - setShowBadge(false) - build() - } - ) - RadioNotificationService.events.subscribe { - when (it) { - RadioNotificationService.Event.START -> onServiceStart() - RadioNotificationService.Event.STOP -> onServiceStop() - } - } - } - - fun cancel() { - destroyNotification() - RadioNotificationService.destroy() - } - - fun notify(notification: Notification) { - if (!hasService) { - createService() - lastNotification = notification - return - } - try { - manager.notify(RadioNotification.NOTIFICATION_ID, notification) - } catch (_: SecurityException) { - // NOTE: the notification updates even without permission... - } - } - - private fun destroyNotification() { - if (state == State.DESTROYED) { - return - } - state = State.DESTROYED - lastNotification = null - manager.cancel(RadioNotification.CHANNEL_ID, RadioNotification.NOTIFICATION_ID) - } - - private fun createService() { - if (hasService || state == State.PREPARING) { - return - } - val intent = Intent(symphony.applicationContext, RadioNotificationService::class.java) - symphony.applicationContext.startForegroundService(intent) - state = State.PREPARING - } - - private fun onServiceStart() { - state = State.READY - lastNotification?.let { notification -> - lastNotification = null - ServiceCompat.startForeground( - service!!, - RadioNotification.NOTIFICATION_ID, - notification, - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK - } else { - 0 - } - ) - } - } - - private fun onServiceStop() { - destroyNotification() - } -} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioNotificationService.kt b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioNotificationService.kt deleted file mode 100644 index a14fcd28..00000000 --- a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioNotificationService.kt +++ /dev/null @@ -1,44 +0,0 @@ -package io.github.zyrouge.symphony.services.radio - -import android.app.Service -import android.app.Service.START_NOT_STICKY -import android.app.Service.STOP_FOREGROUND_REMOVE -import android.content.Intent -import android.os.IBinder -import io.github.zyrouge.symphony.utils.Eventer - -class RadioNotificationService : Service() { - enum class Event { - START, - STOP, - } - - override fun onBind(p0: Intent?): IBinder? = null - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - instance = this - events.dispatch(Event.START) - return START_NOT_STICKY - } - - override fun onDestroy() { - super.onDestroy() - destroy(false) - } - - companion object { - val events = Eventer() - var instance: RadioNotificationService? = null - - fun destroy(stop: Boolean = true) { - instance?.let { - instance = null - if (stop) { - it.stopForeground(STOP_FOREGROUND_REMOVE) - it.stopSelf() - } - events.dispatch(Event.STOP) - } - } - } -} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioObservatory.kt b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioObservatory.kt deleted file mode 100644 index eceb69e7..00000000 --- a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioObservatory.kt +++ /dev/null @@ -1,107 +0,0 @@ -package io.github.zyrouge.symphony.services.radio - -import io.github.zyrouge.symphony.Symphony -import io.github.zyrouge.symphony.utils.EventUnsubscribeFn -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update - -class RadioObservatory(private val symphony: Symphony) { - private var updateSubscriber: EventUnsubscribeFn? = null - private var playbackPositionUpdateSubscriber: EventUnsubscribeFn? = null - - private val _isPlaying = MutableStateFlow(false) - val isPlaying = _isPlaying.asStateFlow() - private val _playbackPosition = MutableStateFlow(RadioPlayer.PlaybackPosition.zero) - val playbackPosition = _playbackPosition.asStateFlow() - private val _queueIndex = MutableStateFlow(-1) - val queueIndex = _queueIndex.asStateFlow() - private val _queue = MutableStateFlow(emptyList()) - val queue = _queue.asStateFlow() - private val _loopMode = MutableStateFlow(RadioQueue.LoopMode.None) - val loopMode = _loopMode.asStateFlow() - private val _shuffleMode = MutableStateFlow(false) - val shuffleMode = _shuffleMode.asStateFlow() - private val _sleepTimer = MutableStateFlow(null) - val sleepTimer = _sleepTimer.asStateFlow() - private val _pauseOnCurrentSongEnd = MutableStateFlow(false) - val pauseOnCurrentSongEnd = _pauseOnCurrentSongEnd.asStateFlow() - private val _speed = MutableStateFlow(RadioPlayer.DEFAULT_SPEED) - val speed = _speed.asStateFlow() - private val _persistedSpeed = MutableStateFlow(RadioPlayer.DEFAULT_SPEED) - val persistedSpeed = _persistedSpeed.asStateFlow() - private val _pitch = MutableStateFlow(RadioPlayer.DEFAULT_PITCH) - val pitch = _pitch.asStateFlow() - private val _persistedPitch = MutableStateFlow(RadioPlayer.DEFAULT_PITCH) - val persistedPitch = _persistedPitch.asStateFlow() - - fun start() { - updateSubscriber = symphony.radio.onUpdate.subscribe { event -> - when (event) { - Radio.Events.Player.Seeked -> emitPlaybackPosition() - is Radio.Events.Player -> emitIsPlaying() - is Radio.Events.Queue.IndexChanged -> emitQueueIndex() - is Radio.Events.Queue -> emitQueue() - Radio.Events.QueueOption.LoopModeChanged -> emitLoopMode() - Radio.Events.QueueOption.ShuffleModeChanged -> emitShuffleMode() - Radio.Events.QueueOption.SleepTimerChanged -> emitSleepTimer() - Radio.Events.QueueOption.SpeedChanged -> emitSpeed() - Radio.Events.QueueOption.PitchChanged -> emitPitch() - Radio.Events.QueueOption.PauseOnCurrentSongEndChanged -> emitPauseOnCurrentSongEnd() - } - } - playbackPositionUpdateSubscriber = symphony.radio.onPlaybackPositionUpdate.subscribe { - emitPlaybackPosition() - } - } - - fun destroy() { - updateSubscriber?.invoke() - playbackPositionUpdateSubscriber?.invoke() - } - - private fun emitIsPlaying() = _isPlaying.update { - symphony.radio.isPlaying - } - - private fun emitPlaybackPosition() = _playbackPosition.update { - symphony.radio.currentPlaybackPosition ?: RadioPlayer.PlaybackPosition.zero - } - - private fun emitQueueIndex() = _queueIndex.update { - symphony.radio.queue.currentSongIndex - } - - private fun emitLoopMode() = _loopMode.update { - symphony.radio.queue.currentLoopMode - } - - private fun emitShuffleMode() = _shuffleMode.update { - symphony.radio.queue.currentShuffleMode - } - - private fun emitSleepTimer() = _sleepTimer.update { - symphony.radio.sleepTimer - } - - private fun emitSpeed() { - _speed.update { symphony.radio.currentSpeed } - _persistedSpeed.update { symphony.radio.persistedSpeed } - } - - fun emitPitch() { - _pitch.update { symphony.radio.currentPitch } - _persistedPitch.update { symphony.radio.persistedPitch } - } - - - private fun emitPauseOnCurrentSongEnd() = _pauseOnCurrentSongEnd.update { - symphony.radio.pauseOnCurrentSongEnd - } - - private fun emitQueue() { - _queue.update { - symphony.radio.queue.currentQueue.toList() - } - } -} diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioPlayer.kt b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioPlayer.kt index 2220ed82..aa041e5b 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioPlayer.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioPlayer.kt @@ -1,19 +1,28 @@ package io.github.zyrouge.symphony.services.radio -import android.media.MediaPlayer -import android.media.PlaybackParams import android.net.Uri +import android.os.Looper +import androidx.annotation.OptIn +import androidx.media3.common.AudioAttributes +import androidx.media3.common.C +import androidx.media3.common.ForwardingPlayer +import androidx.media3.common.MediaItem +import androidx.media3.common.PlaybackParameters +import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.session.MediaSession import io.github.zyrouge.symphony.Symphony -import io.github.zyrouge.symphony.utils.Logger +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.util.Timer -typealias RadioPlayerOnPreparedListener = () -> Unit -typealias RadioPlayerOnPlaybackPositionListener = (RadioPlayer.PlaybackPosition) -> Unit -typealias RadioPlayerOnFinishListener = () -> Unit -typealias RadioPlayerOnErrorListener = (Int, Int) -> Unit - -class RadioPlayer(val symphony: Symphony, val id: String, val uri: Uri) { +@OptIn(UnstableApi::class) +class RadioPlayer(val symphony: Symphony) { data class PlaybackPosition(val played: Long, val total: Long) { val ratio: Float get() = (played.toFloat() / total).takeIf { it.isFinite() } ?: 0f @@ -23,219 +32,183 @@ class RadioPlayer(val symphony: Symphony, val id: String, val uri: Uri) { } } - enum class State { - Unprepared, - Preparing, - Prepared, - Finished, - Destroyed, - } - - private val unsafeMediaPlayer: MediaPlayer - private val mediaPlayer: MediaPlayer? get() = if (usable) unsafeMediaPlayer else null - private var onPrepared: RadioPlayerOnPreparedListener? = null - private var onPlaybackPosition: RadioPlayerOnPlaybackPositionListener? = null - private var onFinish: RadioPlayerOnFinishListener? = null - private var onError: RadioPlayerOnErrorListener? = null - private var fader: RadioEffects.Fader? = null - private var playbackPositionUpdater: Timer? = null + private class ExoPlayerListener(val player: RadioPlayer) : Player.Listener { + override fun onIsPlayingChanged(isPlaying: Boolean) { + player.onMediaPlayerIsPlayingChanged(isPlaying) + } - var state = State.Unprepared - private set - var hasPlayedOnce = false - private set - var volume = MAX_VOLUME - private set - var speed = DEFAULT_SPEED - private set - var pitch = DEFAULT_PITCH - private set - - val usable get() = state == State.Prepared - val fadePlayback get() = symphony.settings.fadePlayback.value - val audioSessionId get() = mediaPlayer?.audioSessionId - val isPlaying get() = mediaPlayer?.isPlaying == true - - val playbackPosition - get() = mediaPlayer?.let { - try { - PlaybackPosition( - played = it.currentPosition.toLong(), - total = it.duration.toLong(), - ) - } catch (_: IllegalStateException) { - null - } + override fun onVolumeChanged(volume: Float) { + player.onMediaPlayerVolumeChanged(volume) } - init { - unsafeMediaPlayer = MediaPlayer().also { ump -> - ump.setOnPreparedListener { - state = State.Prepared - ump.playbackParams.setAudioFallbackMode(PlaybackParams.AUDIO_FALLBACK_MODE_DEFAULT) - createDurationTimer() - onPrepared?.invoke() - } - ump.setOnCompletionListener { - state = State.Finished - onFinish?.invoke() - } - ump.setOnErrorListener { _, what, extra -> - state = State.Destroyed - onError?.invoke(what, extra) - true - } - ump.setDataSource(symphony.applicationContext, uri) + override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters) { + player.onMediaPlayerPlaybackParametersChanged(playbackParameters) } - } - fun prepare() { - when (state) { - State.Unprepared -> { - unsafeMediaPlayer.prepareAsync() - state = State.Preparing - } + override fun onPlaybackStateChanged(playbackState: Int) { + player.onMediaPlayerPlaybackStateChanged(playbackState) + } - State.Prepared -> onPrepared?.invoke() - else -> {} + override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { + player.onMediaPlayerMediaItemTransition(mediaItem, reason) } } - fun stop() = destroy() + private class InterceptedMediaPlayer(val player: RadioPlayer, mediaPlayer: ExoPlayer) : + ForwardingPlayer(mediaPlayer) + + private class MediaSessionCallback(val player: RadioPlayer) : MediaSession.Callback + + private val mediaPlayerListener = ExoPlayerListener(this) + private val mediaPlayerUnsafe = ExoPlayer.Builder(symphony.applicationContext) + .setLooper(Looper.getMainLooper()) + .setAudioAttributes(AudioAttributes.DEFAULT, true) + .setHandleAudioBecomingNoisy(true) + .setWakeMode(C.WAKE_MODE_LOCAL) + .build() + + private val interceptingMediaPlayer = InterceptedMediaPlayer(this, mediaPlayerUnsafe) + private val mediaSessionCallback = MediaSessionCallback(this) + private val mediaSession = MediaSession + .Builder(symphony.applicationContext, interceptingMediaPlayer) + .setCallback(mediaSessionCallback) + .build() + val mediaSessionId get() = mediaSession.id - fun destroy() { - state = State.Destroyed - destroyDurationTimer() +// val fadePlayback get() = symphony.settings.fadePlayback.value + + private val _isPlaying = MutableStateFlow(false) + val isPlaying = _isPlaying.asStateFlow() + private var playbackPositionUpdater: Timer? = null + private val _playbackPosition = MutableStateFlow(PlaybackPosition.zero) + val playbackPosition = _playbackPosition.asStateFlow() + private val _volume = MutableStateFlow(MAX_VOLUME) + val volume = _volume.asStateFlow() + private val _speed = MutableStateFlow(DEFAULT_SPEED) + val speed = _speed.asStateFlow() + private val _pitch = MutableStateFlow(DEFAULT_PITCH) + val pitch = _pitch.asStateFlow() + + init { symphony.groove.coroutineScope.launch { - unsafeMediaPlayer.stop() - unsafeMediaPlayer.release() + prepare() } } - fun start() = mediaPlayer?.let { - it.start() - createDurationTimer() - if (!hasPlayedOnce) { - hasPlayedOnce = true - changeSpeed(speed) - changePitch(pitch) - } + private suspend fun prepare() = withMediaPlayer { + it.addListener(mediaPlayerListener) + it.prepare() + } + + suspend fun hasMedia() = withMediaPlayer { it.currentMediaItem != null } + + suspend fun hasNextMedia() = withMediaPlayer { it.getMediaItemAt(it.currentMediaItemIndex + 1) } + + suspend fun setMedia(uri: Uri) = withMediaPlayer { + it.setMediaItems(listOf(MediaItem.fromUri(uri))) + } + + suspend fun setNextMedia(uri: Uri) = withMediaPlayer { + it.replaceMediaItem(1, MediaItem.fromUri(uri)) + it.play() + } + + suspend fun play() = withMediaPlayer { + it.play() } - fun pause() = mediaPlayer?.let { + suspend fun pause() = withMediaPlayer { it.pause() - destroyDurationTimer() } - fun seek(to: Int) = mediaPlayer?.let { + suspend fun stop() = withMediaPlayer { + it.stop() + it.clearMediaItems() + } + + suspend fun seek(to: Long) = withMediaPlayer { it.seekTo(to) - emitPlaybackPosition() } - fun changeVolume( - to: Float, - forceFade: Boolean = false, - onFinish: (Boolean) -> Unit, - ) { - fader?.stop() - when { - to == volume -> onFinish(true) - forceFade || fadePlayback -> { - val duration = (symphony.settings.fadePlaybackDuration.value * 1000).toInt() - fader = RadioEffects.Fader( - RadioEffects.Fader.Options(volume, to, duration), - onUpdate = { - changeVolumeInstant(it) - }, - onFinish = { - onFinish(it) - fader = null - } - ) - fader?.start() - } + suspend fun setVolume(to: Float) = withMediaPlayer { + it.volume = to + } - else -> { - changeVolumeInstant(to) - onFinish(true) - } - } + suspend fun setSpeed(to: Float) = withMediaPlayer { + it.playbackParameters = it.playbackParameters.withSpeed(to) } - fun changeVolumeInstant(to: Float) { - volume = to - mediaPlayer?.setVolume(to, to) + suspend fun setPitch(to: Float) = withMediaPlayer { + it.playbackParameters = it.playbackParameters.withPitch(to) } - fun changeSpeed(to: Float) { - if (!hasPlayedOnce) { - speed = to - return + private fun createDurationTimer() { + playbackPositionUpdater = kotlin.concurrent.timer(period = 500L) { + emitPlaybackPosition() } - mediaPlayer?.let { - val isPlaying = it.isPlaying - try { - it.playbackParams = it.playbackParams.setSpeed(to) - speed = to - } catch (err: Exception) { - Logger.error("RadioPlayer", "changing speed failed", err) - } - if (!isPlaying) { - it.pause() + } + + private fun emitPlaybackPosition() { + symphony.groove.coroutineScope.launch(Dispatchers.Main) { + _playbackPosition.update { + PlaybackPosition(mediaPlayerUnsafe.currentPosition, mediaPlayerUnsafe.duration) } } } - fun changePitch(to: Float) { - if (!hasPlayedOnce) { - pitch = to - return - } - mediaPlayer?.let { - val isPlaying = it.isPlaying - try { - it.playbackParams = it.playbackParams.setPitch(to) - pitch = to - } catch (err: Exception) { - Logger.error("RadioPlayer", "changing pitch failed", err) - } - if (!isPlaying) { - it.pause() - } + private fun destroyDurationTimer() { + playbackPositionUpdater?.cancel() + playbackPositionUpdater = null + } + + private suspend fun withMediaPlayer(fn: (ExoPlayer) -> T): T { + return withContext(Dispatchers.Main) { + fn(mediaPlayerUnsafe) } } - fun setOnPreparedListener(listener: RadioPlayerOnPreparedListener?) { - onPrepared = listener + private fun withMediaPlayerNoSuspend(fn: (ExoPlayer) -> Unit) { + symphony.groove.coroutineScope.launch(Dispatchers.Main) { + fn(mediaPlayerUnsafe) + } } - fun setOnPlaybackPositionListener(listener: RadioPlayerOnPlaybackPositionListener?) { - onPlaybackPosition = listener + fun onMediaPlayerIsPlayingChanged(isPlaying: Boolean) { + when { + isPlaying -> createDurationTimer() + else -> destroyDurationTimer() + } + _isPlaying.update { isPlaying } } - fun setOnFinishListener(listener: RadioPlayerOnFinishListener?) { - onFinish = listener + fun onMediaPlayerVolumeChanged(volume: Float) { + _volume.update { volume } } - fun setOnErrorListener(listener: RadioPlayerOnErrorListener?) { - onError = listener + fun onMediaPlayerPlaybackParametersChanged(playbackParameters: PlaybackParameters) { + _speed.update { playbackParameters.speed } + _pitch.update { playbackParameters.pitch } } - private fun createDurationTimer() { - playbackPositionUpdater = kotlin.concurrent.timer(period = 100L) { - emitPlaybackPosition() + fun onMediaPlayerPlaybackStateChanged(playbackState: Int) { + if (playbackState == Player.STATE_ENDED) { + onMediaPlayerMediaEnded() } } - private fun emitPlaybackPosition() { - playbackPosition?.let { - onPlaybackPosition?.invoke(it) - } + fun onMediaPlayerMediaItemTransition(mediaItem: MediaItem?, reason: Int) { + onMediaPlayerMediaEnded() } - private fun destroyDurationTimer() { - playbackPositionUpdater?.cancel() - playbackPositionUpdater = null + private fun onMediaPlayerMediaEnded() { + emitPlaybackPosition() + withMediaPlayerNoSuspend { + // keep playing item at 0 + if (it.mediaItemCount == 0) { + return@withMediaPlayerNoSuspend + } + it.removeMediaItem(0) + } } companion object { diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioQueue.kt b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioQueue.kt index 6e27c321..7e4990af 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioQueue.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioQueue.kt @@ -50,7 +50,7 @@ class RadioQueue(private val symphony: Symphony) { } // val queueFlow = symphony.database.songQueue.findFirstAsFlow() -// val queue = AtomicReference(null) +// val songQueue = AtomicReference(null) // @OptIn(ExperimentalCoroutinesApi::class) // val queueSongsFlow = queueFlow.transformLatest { @@ -75,16 +75,12 @@ class RadioQueue(private val symphony: Symphony) { // } } - suspend fun add( - songIds: List, - insertAfterId: String? = null, - options: Radio.PlayOptions = Radio.PlayOptions(), - ) { - val origQueue = symphony.database.songQueue.findByInternalId(SONG_QUEUE_INTERNAL_ID_DEFAULT) - val queueId = origQueue?.entity?.id ?: symphony.database.songQueueIdGenerator.next() + suspend fun add(songIds: List, insertAfterId: String? = null) { + val origQueue = getCurrentSongQueue() + val songQueueId = origQueue?.entity?.id ?: symphony.database.songQueueIdGenerator.next() if (origQueue == null) { - val queue = SongQueue( - id = queueId, + val songQueue = SongQueue( + id = songQueueId, playingId = null, playingTimestamp = null, playingSpeedInt = SongQueue.SPEED_MULTIPLIER, @@ -95,91 +91,73 @@ class RadioQueue(private val symphony: Symphony) { pitchInt = SongQueue.PITCH_MULTIPLIER, pauseOnSongEnd = false, ) - symphony.database.songQueue.insert(queue) + symphony.database.songQueue.insert(songQueue) } - val operator = createSongQueueSongMappingOperator(queueId) + val operator = createSongQueueSongMappingOperator(songQueueId) operator.append(insertAfterId, songIds) { x, isHead, nextId -> SongQueueSongMapping( id = symphony.database.songQueueSongMappingIdGenerator.next(), - queueId = queueId, + queueId = songQueueId, songId = x, isHead = isHead, nextId = nextId, ogNextId = nextId, ) } - afterAdd(options) - } - - suspend fun add( - songId: String, - insertAfterId: String? = null, - options: Radio.PlayOptions = Radio.PlayOptions(), - ) = add(listOf(songId), insertAfterId, options) - - suspend fun add( - songs: List, - insertAfterId: String? = null, - options: Radio.PlayOptions = Radio.PlayOptions(), - ) = add(songs.map { it.id }, insertAfterId, options) - - suspend fun add( - song: Song, - insertAfterId: String? = null, - options: Radio.PlayOptions = Radio.PlayOptions(), - ) = add(listOf(song.id), insertAfterId, options) - - private fun afterAdd(options: Radio.PlayOptions) { -// if (!symphony.radio.hasPlayer) { -// symphony.radio.play(options) -// } -// symphony.radio.onUpdate.dispatch(Radio.Events.Queue.Modified) + } + + suspend fun add(songId: String, insertAfterId: String? = null) { + add(listOf(songId), insertAfterId) + } + + suspend fun add(songs: List, insertAfterId: String? = null) { + add(songs.map { it.id }, insertAfterId) + } + + suspend fun add(song: Song, insertAfterId: String? = null) { + add(listOf(song.id), insertAfterId) } suspend fun remove(songMappingIds: List): Boolean { - val queue = symphony.database.songQueue.findByInternalId(SONG_QUEUE_INTERNAL_ID_DEFAULT) - if (queue == null) { - return false - } - val queueId = queue.entity.id - val operator = createSongQueueSongMappingOperator(queueId) + val songQueue = getCurrentSongQueue() ?: return false + val songQueueId = songQueue.entity.id + val operator = createSongQueueSongMappingOperator(songQueueId) val result = operator.remove(songMappingIds) return result.deletedKeys.isNotEmpty() } - suspend fun remove(songMappingId: String) = remove(listOf(songMappingId)) + suspend fun remove(songMappingId: String) { + remove(listOf(songMappingId)) + } + + suspend fun clear(): Boolean { + val songQueue = getCurrentSongQueue() ?: return false + symphony.database.songQueueSongMapping.deleteAll(songQueue.entity.id) + return true + } - private suspend fun setLoopMode(queue: SongQueue, loopMode: SongQueue.LoopMode) { - val nQueue = queue.copy(loopMode = loopMode) - symphony.database.songQueue.update(nQueue) + private suspend fun setLoopMode(songQueue: SongQueue, loopMode: SongQueue.LoopMode) { + val nSongQueue = songQueue.copy(loopMode = loopMode) + symphony.database.songQueue.update(nSongQueue) } suspend fun setLoopMode(loopMode: SongQueue.LoopMode): Boolean { - val queue = symphony.database.songQueue.findByInternalId(SONG_QUEUE_INTERNAL_ID_DEFAULT) - if (queue == null) { - return false - } - setLoopMode(queue.entity, loopMode) + val songQueue = getCurrentSongQueue() ?: return false + setLoopMode(songQueue.entity, loopMode) return true } suspend fun toggleLoopMode(): Boolean { - val queue = symphony.database.songQueue.findByInternalId(SONG_QUEUE_INTERNAL_ID_DEFAULT) - if (queue == null) { - return false - } - val currentLoopMode = queue.entity.loopMode + val songQueue = getCurrentSongQueue() ?: return false + val currentLoopMode = songQueue.entity.loopMode val nextLoopModeOrdinal = (currentLoopMode.ordinal + 1) % SongQueue.LoopMode.values.size val nextLoopMode = SongQueue.LoopMode.values[nextLoopModeOrdinal] - setLoopMode(queue.entity, nextLoopMode) + setLoopMode(songQueue.entity, nextLoopMode) return true } fun toggleShuffleMode(): Boolean { -// val queue = symphony.database.songQueue.findByInternalId(SONG_QUEUE_INTERNAL_ID_DEFAULT) -// if (queue == null) { -// return false -// } +// val songQueue = getSongQueue() ?: return false // val nQueue = queue.entity.copy(shuffled = !queue.entity.shuffled) // symphony.database.songQueue.update(nQueue) return true @@ -206,6 +184,9 @@ class RadioQueue(private val symphony: Symphony) { // symphony.radio.onUpdate.dispatch(Radio.Events.Queue.Modified) } + fun getCurrentSongQueue() = + symphony.database.songQueue.findByInternalId(SONG_QUEUE_INTERNAL_ID_DEFAULT) + private fun createSongQueueSongMappingOperator(queueId: String) = LazyLinkedListOperatorHelper( SongQueueSongMappingOperatorEntityFunctions(), SongQueueSongMappingOperatorPersistenceFunctions( diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioSession.kt b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioSession.kt index 6567aa7b..86cf4a61 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioSession.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioSession.kt @@ -1,185 +1,173 @@ package io.github.zyrouge.symphony.services.radio -import android.annotation.SuppressLint -import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import android.content.IntentFilter -import android.graphics.Bitmap import android.media.audiofx.AudioEffect -import android.net.Uri -import android.os.Build -import android.support.v4.media.MediaMetadataCompat -import android.support.v4.media.session.MediaSessionCompat -import android.support.v4.media.session.PlaybackStateCompat -import android.view.KeyEvent import androidx.activity.result.contract.ActivityResultContract import io.github.zyrouge.symphony.R import io.github.zyrouge.symphony.Symphony -import io.github.zyrouge.symphony.services.groove.Song -import kotlinx.coroutines.launch class RadioSession(val symphony: Symphony) { - data class UpdateRequest( - val song: Song, - val artworkUri: Uri, - val artworkBitmap: Bitmap, - val playbackPosition: RadioPlayer.PlaybackPosition, - val isPlaying: Boolean, - ) - - internal val mediaSession = MediaSessionCompat(symphony.applicationContext, MEDIA_SESSION_ID) - private val artworkCacher = RadioArtworkCacher(symphony) - private val notification = RadioNotification(symphony) - - private var currentSongId: String? = null - private var receiver = object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - intent?.action?.let { action -> - handleAction(action) - } - } - } +// data class UpdateRequest( +// val song: Song, +// val artworkUri: Uri, +// val artworkBitmap: Bitmap, +// val playbackPosition: RadioPlayer.PlaybackPosition, +// val isPlaying: Boolean, +// ) +// +// internal val mediaSession = MediaSession.Builder(symphony.applicationContext) +// private val artworkCacher = RadioArtworkCacher(symphony) +// private val notification = RadioNotification(symphony) +// +// private var currentSongId: String? = null +// private var receiver = object : BroadcastReceiver() { +// override fun onReceive(context: Context?, intent: Intent?) { +// intent?.action?.let { action -> +// handleAction(action) +// } +// } +// } fun start() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - symphony.applicationContext.registerReceiver( - receiver, - IntentFilter().apply { - addAction(ACTION_PLAY_PAUSE) - addAction(ACTION_PREVIOUS) - addAction(ACTION_NEXT) - addAction(ACTION_STOP) - }, - Context.RECEIVER_EXPORTED, - // https://developer.android.com/reference/android/content/Context#RECEIVER_EXPORTED - // really, RECEIVER_EXPORTED and RECEIVER_NOT_EXPORTED makes no difference. - // the notification appears perfectly, Pano Scrobbler sees it, - // Wear OS can send signals to play/pause the app, other media apps can pause it, - // no clue what the difference here is... but here we are. - ) - } else { - @SuppressLint("UnspecifiedRegisterReceiverFlag") - symphony.applicationContext.registerReceiver( - receiver, - IntentFilter().apply { - addAction(ACTION_PLAY_PAUSE) - addAction(ACTION_PREVIOUS) - addAction(ACTION_NEXT) - addAction(ACTION_STOP) - }, - ) - } - mediaSession.setCallback( - object : MediaSessionCompat.Callback() { - override fun onPlay() { - super.onPlay() - handleAction(ACTION_PLAY_PAUSE) - } - - override fun onPause() { - super.onPause() - handleAction(ACTION_PLAY_PAUSE) - } - - override fun onSkipToPrevious() { - super.onSkipToPrevious() - handleAction(ACTION_PREVIOUS) - } - - override fun onSkipToNext() { - super.onSkipToNext() - handleAction(ACTION_NEXT) - } - - override fun onStop() { - super.onStop() - handleAction(ACTION_STOP) - } - - override fun onSeekTo(pos: Long) { - super.onSeekTo(pos) - symphony.radio.seek(pos) - } - - override fun onRewind() { - super.onRewind() - val duration = symphony.settings.seekBackDuration.value - symphony.radio.shorty.seekFromCurrent(-duration) - } - - override fun onFastForward() { - super.onFastForward() - val duration = symphony.settings.seekForwardDuration.value - symphony.radio.shorty.seekFromCurrent(duration) - } - - override fun onMediaButtonEvent(intent: Intent?): Boolean { - val handled = super.onMediaButtonEvent(intent) - if (handled) { - return true - } - val keyEvent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - intent?.getParcelableExtra( - Intent.EXTRA_KEY_EVENT, - KeyEvent::class.java, - ) - } else { - @Suppress("DEPRECATION") - intent?.getParcelableExtra(Intent.EXTRA_KEY_EVENT) - } - return when (keyEvent?.keyCode) { - KeyEvent.KEYCODE_MEDIA_PREVIOUS, - KeyEvent.KEYCODE_MEDIA_REWIND, - -> { - handleAction(ACTION_PREVIOUS) - true - } - - KeyEvent.KEYCODE_MEDIA_NEXT -> { - handleAction(ACTION_NEXT) - true - } - - KeyEvent.KEYCODE_MEDIA_CLOSE, - KeyEvent.KEYCODE_MEDIA_STOP, - -> { - handleAction(ACTION_STOP) - true - } - - else -> false - } - } - } - ) - notification.start() - symphony.radio.onUpdate.subscribe { - when (it) { - Radio.Events.Player.Ended -> cancel() - is Radio.Events.Player -> update() - else -> {} - } - } +// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { +// symphony.applicationContext.registerReceiver( +// receiver, +// IntentFilter().apply { +// addAction(ACTION_PLAY_PAUSE) +// addAction(ACTION_PREVIOUS) +// addAction(ACTION_NEXT) +// addAction(ACTION_STOP) +// }, +// Context.RECEIVER_EXPORTED, +// // https://developer.android.com/reference/android/content/Context#RECEIVER_EXPORTED +// // really, RECEIVER_EXPORTED and RECEIVER_NOT_EXPORTED makes no difference. +// // the notification appears perfectly, Pano Scrobbler sees it, +// // Wear OS can send signals to play/pause the app, other media apps can pause it, +// // no clue what the difference here is... but here we are. +// ) +// } else { +// @SuppressLint("UnspecifiedRegisterReceiverFlag") +// symphony.applicationContext.registerReceiver( +// receiver, +// IntentFilter().apply { +// addAction(ACTION_PLAY_PAUSE) +// addAction(ACTION_PREVIOUS) +// addAction(ACTION_NEXT) +// addAction(ACTION_STOP) +// }, +// ) +// } +// mediaSession.setCallback( +// object : MediaSessionCompat.Callback() { +// override fun onPlay() { +// super.onPlay() +// handleAction(ACTION_PLAY_PAUSE) +// } +// +// override fun onPause() { +// super.onPause() +// handleAction(ACTION_PLAY_PAUSE) +// } +// +// override fun onSkipToPrevious() { +// super.onSkipToPrevious() +// handleAction(ACTION_PREVIOUS) +// } +// +// override fun onSkipToNext() { +// super.onSkipToNext() +// handleAction(ACTION_NEXT) +// } +// +// override fun onStop() { +// super.onStop() +// handleAction(ACTION_STOP) +// } +// +// override fun onSeekTo(pos: Long) { +// super.onSeekTo(pos) +// symphony.radio.seek(pos) +// } +// +// override fun onRewind() { +// super.onRewind() +// val duration = symphony.settings.seekBackDuration.value +// symphony.radio.shorty.seekFromCurrent(-duration) +// } +// +// override fun onFastForward() { +// super.onFastForward() +// val duration = symphony.settings.seekForwardDuration.value +// symphony.radio.shorty.seekFromCurrent(duration) +// } +// +// override fun onMediaButtonEvent(intent: Intent?): Boolean { +// val handled = super.onMediaButtonEvent(intent) +// if (handled) { +// return true +// } +// val keyEvent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { +// intent?.getParcelableExtra( +// Intent.EXTRA_KEY_EVENT, +// KeyEvent::class.java, +// ) +// } else { +// @Suppress("DEPRECATION") +// intent?.getParcelableExtra(Intent.EXTRA_KEY_EVENT) +// } +// return when (keyEvent?.keyCode) { +// KeyEvent.KEYCODE_MEDIA_PREVIOUS, +// KeyEvent.KEYCODE_MEDIA_REWIND, +// -> { +// handleAction(ACTION_PREVIOUS) +// true +// } +// +// KeyEvent.KEYCODE_MEDIA_NEXT -> { +// handleAction(ACTION_NEXT) +// true +// } +// +// KeyEvent.KEYCODE_MEDIA_CLOSE, +// KeyEvent.KEYCODE_MEDIA_STOP, +// -> { +// handleAction(ACTION_STOP) +// true +// } +// +// else -> false +// } +// } +// } +// ) +// notification.start() +// symphony.radio.onUpdate.subscribe { +// when (it) { +// Radio.Events.Player.Ended -> cancel() +// is Radio.Events.Player -> update() +// else -> {} +// } +// } } fun handleAction(action: String) { - when (action) { - ACTION_PLAY_PAUSE -> symphony.radio.shorty.playPause() - ACTION_PREVIOUS -> symphony.radio.shorty.previous() - ACTION_NEXT -> symphony.radio.shorty.skip() - ACTION_STOP -> symphony.radio.stop() - } +// when (action) { +// ACTION_PLAY_PAUSE -> symphony.radio.shorty.playPause() +// ACTION_PREVIOUS -> symphony.radio.shorty.previous() +// ACTION_NEXT -> symphony.radio.shorty.skip() +// ACTION_STOP -> symphony.radio.stop() +// } } fun cancel() { - notification.cancel() - mediaSession.isActive = false +// notification.cancel() +// mediaSession.isActive = false } fun destroy() { - cancel() - symphony.applicationContext.unregisterReceiver(receiver) +// cancel() +// symphony.applicationContext.unregisterReceiver(receiver) } fun createEqualizerActivityContract() = object : ActivityResultContract() { @@ -188,7 +176,7 @@ class RadioSession(val symphony: Symphony) { input: Unit, ) = Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL).apply { putExtra(AudioEffect.EXTRA_PACKAGE_NAME, symphony.applicationContext.packageName) - putExtra(AudioEffect.EXTRA_AUDIO_SESSION, symphony.radio.audioSessionId ?: 0) + putExtra(AudioEffect.EXTRA_AUDIO_SESSION, symphony.radio.player.mediaSessionId) putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC) } @@ -200,90 +188,90 @@ class RadioSession(val symphony: Symphony) { } private fun update() { - symphony.groove.coroutineScope.launch { - updateAsync() - } +// symphony.groove.coroutineScope.launch { +// updateAsync() +// } } private suspend fun updateAsync() { - val song = symphony.radio.queue.currentSongId - ?.let { symphony.groove.song.get(it) } ?: return - currentSongId = song.id - val artworkUri = symphony.groove.song.getArtworkUri(song.id) - val artworkBitmap = artworkCacher.getArtwork(song) - val playbackPosition = symphony.radio.currentPlaybackPosition - ?: RadioPlayer.PlaybackPosition(played = 0L, total = song.duration) - val isPlaying = symphony.radio.isPlaying - if (currentSongId != song.id) { - return - } - val req = UpdateRequest( - song = song, - artworkUri = artworkUri, - artworkBitmap = artworkBitmap, - playbackPosition = playbackPosition, - isPlaying = isPlaying, - ) - updateSession(req) - notification.update(req) - } - - private fun updateSession(req: UpdateRequest) { - ensureEnabled() - mediaSession.run { - setMetadata( - MediaMetadataCompat.Builder().run { - putString(MediaMetadataCompat.METADATA_KEY_TITLE, req.song.title) - if (req.song.artists.isNotEmpty()) { - putString( - MediaMetadataCompat.METADATA_KEY_ARTIST, - req.song.artists.joinToString() - ) - } - putString(MediaMetadataCompat.METADATA_KEY_ALBUM, req.song.album) - req.artworkBitmap.let { - putBitmap(MediaMetadataCompat.METADATA_KEY_ART, it) - putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, it) - putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, it) - } - putLong( - MediaMetadataCompat.METADATA_KEY_DURATION, - req.playbackPosition.total.toLong() - ) - build() - } - ) - setPlaybackState( - PlaybackStateCompat.Builder().run { - setState( - when { - req.isPlaying -> PlaybackStateCompat.STATE_PLAYING - else -> PlaybackStateCompat.STATE_PAUSED - }, - req.playbackPosition.played.toLong(), - 1f - ) - setActions( - PlaybackStateCompat.ACTION_PLAY - or PlaybackStateCompat.ACTION_PAUSE - or PlaybackStateCompat.ACTION_PLAY_PAUSE - or PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS - or PlaybackStateCompat.ACTION_SKIP_TO_NEXT - or PlaybackStateCompat.ACTION_STOP - or PlaybackStateCompat.ACTION_REWIND - or PlaybackStateCompat.ACTION_FAST_FORWARD - or PlaybackStateCompat.ACTION_SEEK_TO - ) - build() - } - ) - } +// val song = symphony.radio.queue.currentSongId +// ?.let { symphony.groove.song.get(it) } ?: return +// currentSongId = song.id +// val artworkUri = symphony.groove.song.getArtworkUri(song.id) +// val artworkBitmap = artworkCacher.getArtwork(song) +// val playbackPosition = symphony.radio.currentPlaybackPosition +// ?: RadioPlayer.PlaybackPosition(played = 0L, total = song.duration) +// val isPlaying = symphony.radio.isPlaying +// if (currentSongId != song.id) { +// return +// } +// val req = UpdateRequest( +// song = song, +// artworkUri = artworkUri, +// artworkBitmap = artworkBitmap, +// playbackPosition = playbackPosition, +// isPlaying = isPlaying, +// ) +// updateSession(req) +// notification.update(req) +// } +// +// private fun updateSession(req: UpdateRequest) { +// ensureEnabled() +// mediaSession.run { +// setMetadata( +// MediaMetadataCompat.Builder().run { +// putString(MediaMetadataCompat.METADATA_KEY_TITLE, req.song.title) +// if (req.song.artists.isNotEmpty()) { +// putString( +// MediaMetadataCompat.METADATA_KEY_ARTIST, +// req.song.artists.joinToString() +// ) +// } +// putString(MediaMetadataCompat.METADATA_KEY_ALBUM, req.song.album) +// req.artworkBitmap.let { +// putBitmap(MediaMetadataCompat.METADATA_KEY_ART, it) +// putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, it) +// putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, it) +// } +// putLong( +// MediaMetadataCompat.METADATA_KEY_DURATION, +// req.playbackPosition.total.toLong() +// ) +// build() +// } +// ) +// setPlaybackState( +// PlaybackStateCompat.Builder().run { +// setState( +// when { +// req.isPlaying -> PlaybackStateCompat.STATE_PLAYING +// else -> PlaybackStateCompat.STATE_PAUSED +// }, +// req.playbackPosition.played.toLong(), +// 1f +// ) +// setActions( +// PlaybackStateCompat.ACTION_PLAY +// or PlaybackStateCompat.ACTION_PAUSE +// or PlaybackStateCompat.ACTION_PLAY_PAUSE +// or PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS +// or PlaybackStateCompat.ACTION_SKIP_TO_NEXT +// or PlaybackStateCompat.ACTION_STOP +// or PlaybackStateCompat.ACTION_REWIND +// or PlaybackStateCompat.ACTION_FAST_FORWARD +// or PlaybackStateCompat.ACTION_SEEK_TO +// ) +// build() +// } +// ) +// } } private fun ensureEnabled() { - if (!mediaSession.isActive) { - mediaSession.isActive = true - } +// if (!mediaSession.isActive) { +// mediaSession.isActive = true +// } } companion object { diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingBodyContent.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingBodyContent.kt index be1b0d0a..774b7765 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingBodyContent.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingBodyContent.kt @@ -50,6 +50,7 @@ import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.zyrouge.symphony.ui.components.SongDropdownMenu import io.github.zyrouge.symphony.ui.helpers.FadeTransition import io.github.zyrouge.symphony.ui.helpers.ViewContext @@ -288,7 +289,7 @@ fun NowPlayingTraditionalControls(context: ViewContext, data: NowPlayingData) { @Composable fun NowPlayingSeekBar(context: ViewContext) { - val playbackPosition by context.symphony.radio.observatory.playbackPosition.collectAsStateWithLifecycle() + val playbackPosition by context.symphony.radio.player.playbackPosition.collectAsStateWithLifecycle() Row( modifier = Modifier.padding(defaultHorizontalPadding, 0.dp), diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListOperatorHelper.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListOperatorHelper.kt index 8d9a7266..118b007a 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListOperatorHelper.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListOperatorHelper.kt @@ -34,7 +34,7 @@ class LazyLinkedListOperatorHelper( suspend fun prependHead( values: List, createFn: LazyLinkedListPrependHeadOperatorCreateFn, - ) = LazyLinkedListPrependHeadOperator(this, values, createFn).operate() + ) = LazyLinkedListPrependMoveOperator(this, values, createFn).operate() suspend fun append( insertAfterId: K?, From f4edc45ce088d8322b34612f452696f7c19f667c Mon Sep 17 00:00:00 2001 From: Zyrouge Date: Mon, 15 Sep 2025 23:31:00 +0530 Subject: [PATCH 13/15] better radio queue & shuffle support --- .../store/SongQueueSongMappingStore.kt | 97 ++++-- .../services/database/store/SongQueueStore.kt | 35 +- .../services/groove/entities/SongQueue.kt | 17 +- .../groove/entities/SongQueueSongMapping.kt | 10 - .../zyrouge/symphony/services/radio/Radio.kt | 274 +++++++++++---- .../symphony/services/radio/RadioPlayer.kt | 87 ++--- .../symphony/services/radio/RadioQueue.kt | 324 +++++++++++++----- .../LazyLinkedListAppendOperation.kt | 46 +++ .../LazyLinkedListAppendOperator.kt | 50 --- ...torHelper.kt => LazyLinkedListOperator.kt} | 42 ++- ... => LazyLinkedListPrependHeadOperation.kt} | 32 +- .../LazyLinkedListRemoveOperation.kt | 63 ++++ .../LazyLinkedListRemoveOperator.kt | 65 ---- 13 files changed, 742 insertions(+), 400 deletions(-) create mode 100644 app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListAppendOperation.kt delete mode 100644 app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListAppendOperator.kt rename app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/{LazyLinkedListOperatorHelper.kt => LazyLinkedListOperator.kt} (53%) rename app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/{LazyLinkedListPrependHeadOperator.kt => LazyLinkedListPrependHeadOperation.kt} (50%) create mode 100644 app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListRemoveOperation.kt delete mode 100644 app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListRemoveOperator.kt diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueSongMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueSongMappingStore.kt index f2fe032d..f009c9cf 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueSongMappingStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueSongMappingStore.kt @@ -54,7 +54,7 @@ abstract class SongQueueSongMappingStore { } @RawQuery - protected abstract fun findByNextIdRaw(query: SupportSQLiteQuery): Song.AlongSongQueueMapping? + protected abstract fun findByNextId(query: SupportSQLiteQuery): Song.AlongSongQueueMapping? fun findByNextId(queueId: String, nextId: String?): Song.AlongSongQueueMapping? { val query = "SELECT ${Song.TABLE}.*, " + @@ -63,11 +63,24 @@ abstract class SongQueueSongMappingStore { "WHERE ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_QUEUE_ID} = ? AND ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_NEXT_ID} = ? " + "LEFT JOIN ${Song.TABLE} ON ${Song.TABLE}.${Song.COLUMN_ID} = ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_SONG_ID} " val args = arrayOf(queueId, nextId) - return findByNextIdRaw(SimpleSQLiteQuery(query, args)) + return findByNextId(SimpleSQLiteQuery(query, args)) } @RawQuery - protected abstract fun findHeadRaw(query: SupportSQLiteQuery): Song.AlongSongQueueMapping? + protected abstract fun findBySongId(query: SupportSQLiteQuery): Song.AlongSongQueueMapping? + + fun findBySongId(queueId: String, songId: String?): Song.AlongSongQueueMapping? { + val query = "SELECT ${Song.TABLE}.*, " + + "${SongQueueSongMapping.TABLE}.* " + + "FROM ${SongQueueSongMapping.TABLE} " + + "WHERE ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_QUEUE_ID} = ? AND ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_SONG_ID} = ? " + + "LEFT JOIN ${Song.TABLE} ON ${Song.TABLE}.${Song.COLUMN_ID} = ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_SONG_ID} " + val args = arrayOf(queueId, songId) + return findBySongId(SimpleSQLiteQuery(query, args)) + } + + @RawQuery + protected abstract fun findHead(query: SupportSQLiteQuery): Song.AlongSongQueueMapping? fun findHead(queueId: String): Song.AlongSongQueueMapping? { val query = "SELECT ${Song.TABLE}.*, " + @@ -76,10 +89,25 @@ abstract class SongQueueSongMappingStore { "WHERE ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_QUEUE_ID} = ? AND ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_IS_HEAD} = true " + "LEFT JOIN ${Song.TABLE} ON ${Song.TABLE}.${Song.COLUMN_ID} = ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_SONG_ID} " val args = arrayOf(queueId) - return findHeadRaw(SimpleSQLiteQuery(query, args)) + return findHead(SimpleSQLiteQuery(query, args)) + } + + protected abstract fun entries(query: SupportSQLiteQuery): Map< + @MapColumn(SongQueueSongMapping.COLUMN_ID) String, Song.AlongSongQueueMapping> + + fun entries(queueId: String): Map< + String, Song.AlongSongQueueMapping> { + val query = "SELECT ${Song.TABLE}.*, " + + "${SongQueueSongMapping.TABLE}.* " + + "FROM ${SongQueueSongMapping.TABLE} " + + "WHERE ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_QUEUE_ID} = ? " + + "LEFT JOIN ${Song.TABLE} ON ${Song.TABLE}.${Song.COLUMN_ID} = ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_SONG_ID} " + + "ORDER BY ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_IS_HEAD} DESC" + val args = arrayOf(queueId) + return entries(SimpleSQLiteQuery(query, args)) } - protected abstract fun entriesByIdsRaw(query: SupportSQLiteQuery): Map< + protected abstract fun entriesByIds(query: SupportSQLiteQuery): Map< @MapColumn(SongQueueSongMapping.COLUMN_ID) String, Song.AlongSongQueueMapping> fun entriesByIds(queueId: String, songMappingIds: List): Map< @@ -93,10 +121,10 @@ abstract class SongQueueSongMappingStore { "LEFT JOIN ${Song.TABLE} ON ${Song.TABLE}.${Song.COLUMN_ID} = ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_SONG_ID} " + "ORDER BY ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_IS_HEAD} DESC" val args = arrayOf(queueId, *songMappingIds.toTypedArray()) - return entriesByIdsRaw(SimpleSQLiteQuery(query, args)) + return entriesByIds(SimpleSQLiteQuery(query, args)) } - protected abstract fun entriesByNextIdsRaw(query: SupportSQLiteQuery): Map< + protected abstract fun entriesByNextIds(query: SupportSQLiteQuery): Map< @MapColumn(SongQueueSongMapping.COLUMN_NEXT_ID) String, Song.AlongSongQueueMapping> fun entriesByNextIds(queueId: String, songMappingIds: List): Map< @@ -110,7 +138,24 @@ abstract class SongQueueSongMappingStore { "LEFT JOIN ${Song.TABLE} ON ${Song.TABLE}.${Song.COLUMN_ID} = ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_SONG_ID} " + "ORDER BY ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_IS_HEAD} DESC" val args = arrayOf(queueId, *songMappingIds.toTypedArray()) - return entriesByNextIdsRaw(SimpleSQLiteQuery(query, args)) + return entriesByNextIds(SimpleSQLiteQuery(query, args)) + } + + protected abstract fun entriesBySongIds(query: SupportSQLiteQuery): Map< + @MapColumn(SongQueueSongMapping.COLUMN_NEXT_ID) String, Song.AlongSongQueueMapping> + + fun entriesBySongIds(queueId: String, songIds: List): Map< + String, Song.AlongSongQueueMapping> { + val query = "SELECT ${Song.TABLE}.*, " + + "${SongQueueSongMapping.TABLE}.* " + + "FROM ${SongQueueSongMapping.TABLE} " + + "WHERE ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_QUEUE_ID} = ? " + + "AND ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_SONG_ID} " + + "IN (${sqlqph(songIds.size)}) " + + "LEFT JOIN ${Song.TABLE} ON ${Song.TABLE}.${Song.COLUMN_ID} = ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_SONG_ID} " + + "ORDER BY ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_IS_HEAD} DESC" + val args = arrayOf(queueId, *songIds.toTypedArray()) + return entriesBySongIds(SimpleSQLiteQuery(query, args)) } @RawQuery(observedEntities = [Song::class, SongQueueSongMapping::class]) @@ -129,24 +174,32 @@ abstract class SongQueueSongMappingStore { } @OptIn(ExperimentalCoroutinesApi::class) - fun transformEntriesAsValuesFlow(entries: Flow>): Flow> { - return entries.mapLatest { - val list = mutableListOf() - var head = it.firstNotNullOfOrNull { - when { - it.value.mapping.isHead -> it.value - else -> null - } - } - while (head != null) { - list.add(head.entity) - head = it[head.mapping.nextId] + fun transformEntriesAsValues(entries: Map): List { + val list = mutableListOf() + var head = entries.firstNotNullOfOrNull { + when { + it.value.mapping.isHead -> it.value + else -> null } - list.toList() } + while (head != null) { + list.add(head) + head = entries[head.mapping.nextId] + } + return list.toList() + } + + @OptIn(ExperimentalCoroutinesApi::class) + fun transformEntriesAsValuesFlow(entriesFlow: Flow>): Flow> { + return entriesFlow.mapLatest { transformEntriesAsValues(it) } + } + + fun values(queueId: String): List { + val entries = entries(queueId) + return transformEntriesAsValues(entries) } - fun valuesAsFlow(queueId: String): Flow> { + fun valuesAsFlow(queueId: String): Flow> { val entries = entriesAsFlow(queueId) return transformEntriesAsValuesFlow(entries) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueStore.kt index c67bbe8d..256565ab 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueStore.kt @@ -26,36 +26,35 @@ abstract class SongQueueStore { return delete(SimpleSQLiteQuery(query, ids)) } + @RawQuery + protected abstract fun findById(query: SimpleSQLiteQuery): SongQueue.AlongAttributes? + + fun findById(id: String): SongQueue.AlongAttributes? { + val query = "SELECT * FROM ${SongQueue.TABLE} WHERE ${SongQueue.COLUMN_ID} = ?" + val args = arrayOf(id) + return findById(SimpleSQLiteQuery(query, args)) + } + @RawQuery protected abstract fun findByInternalId(query: SimpleSQLiteQuery): SongQueue.AlongAttributes? fun findByInternalId(internalId: Int): SongQueue.AlongAttributes? { - val query = "SELECT * FROM ${SongQueue.TABLE} " + - "WHERE ${SongQueue.COLUMN_INTERNAL_ID} = ?" + val query = "SELECT ${SongQueue.TABLE}.*, " + + "COUNT(${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_SONG_ID}) as ${SongQueue.AlongAttributes.EMBEDDED_TRACKS_COUNT} " + + "FROM ${SongQueue.TABLE} " + + "LEFT JOIN ${SongQueueSongMapping.TABLE} ON ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_QUEUE_ID} = ${SongQueue.TABLE}.${SongQueue.COLUMN_ID}" val args = arrayOf(internalId) return findByInternalId(SimpleSQLiteQuery(query, args)) } @RawQuery(observedEntities = [SongQueue::class, SongQueueSongMapping::class]) - protected abstract fun findFirstAsFlow(query: SupportSQLiteQuery): Flow + protected abstract fun findByInternalIdAsFlow(query: SupportSQLiteQuery): Flow - fun findFirstAsFlow(): Flow { + fun findByInternalIdAsFlow(): Flow { val query = "SELECT ${SongQueue.TABLE}.*, " + "COUNT(${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_SONG_ID}) as ${SongQueue.AlongAttributes.EMBEDDED_TRACKS_COUNT} " + "FROM ${SongQueue.TABLE} " + - "LEFT JOIN ${SongQueueSongMapping.TABLE} ON ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_QUEUE_ID} = ${SongQueue.TABLE}.${SongQueue.COLUMN_ID} " + - "LIMIT 1" - return findFirstAsFlow(SimpleSQLiteQuery(query)) + "LEFT JOIN ${SongQueueSongMapping.TABLE} ON ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_QUEUE_ID} = ${SongQueue.TABLE}.${SongQueue.COLUMN_ID}" + return findByInternalIdAsFlow(SimpleSQLiteQuery(query)) } - -// @RawQuery(observedEntities = [SongQueue::class, SongQueueSongMapping::class]) -// fun valuesAsFlowRaw(query: SupportSQLiteQuery): Flow> - -//fun SongQueueStore.valuesAsFlow(): Flow> { -// val query = "SELECT ${SongQueue.TABLE}.*, " + -// "COUNT(${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_SONG_ID}) as ${SongQueue.AlongAttributes.EMBEDDED_TRACKS_COUNT} " + -// "FROM ${SongQueue.TABLE} " + -// "LEFT JOIN ${SongQueueSongMapping.TABLE} ON ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_QUEUE_ID} = ${SongQueue.TABLE}.${SongQueue.COLUMN_ID}" -// return valuesAsFlowRaw(SimpleSQLiteQuery(query)) -//} } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueue.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueue.kt index 8273dc54..5ee76959 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueue.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueue.kt @@ -12,6 +12,12 @@ import androidx.room.PrimaryKey @Entity( SongQueue.TABLE, foreignKeys = [ + ForeignKey( + entity = SongQueue::class, + parentColumns = arrayOf(SongQueue.COLUMN_ID), + childColumns = arrayOf(SongQueue.COLUMN_ORIGINAL_ID), + onDelete = ForeignKey.SET_NULL, + ), ForeignKey( entity = SongQueueSongMapping::class, parentColumns = arrayOf(SongQueueSongMapping.COLUMN_ID), @@ -25,12 +31,16 @@ data class SongQueue( @PrimaryKey @ColumnInfo(COLUMN_ID) val id: String, + @ColumnInfo(COLUMN_ORIGINAL_ID) + val originalId: String? = null, @ColumnInfo(COLUMN_INTERNAL_ID) val internalId: Int? = null, @ColumnInfo(COLUMN_PLAYING_ID) val playingId: String?, + @ColumnInfo(COLUMN_PLAYING_IS_PLAYING) + val isPlaying: Boolean, @ColumnInfo(COLUMN_PLAYING_TIMESTAMP) - val playingTimestamp: Long?, + val playingTimestamp: Long, @ColumnInfo(COLUMN_PLAYING_SPEED_INT) val playingSpeedInt: Int, @ColumnInfo(COLUMN_PLAYING_PITCH_INT) @@ -45,6 +55,8 @@ data class SongQueue( val pitchInt: Int, @ColumnInfo(COLUMN_PAUSE_ON_SONG_END) val pauseOnSongEnd: Boolean, + @ColumnInfo(COLUMN_SLEEP_TIMER_ENDS_AT) + val sleepTimerEndsAt: Long?, ) { enum class LoopMode { None, @@ -73,8 +85,10 @@ data class SongQueue( companion object { const val TABLE = "song_queue" const val COLUMN_ID = "id" + const val COLUMN_ORIGINAL_ID = "original_id" const val COLUMN_INTERNAL_ID = "internal_id" const val COLUMN_PLAYING_ID = "playing_id" + const val COLUMN_PLAYING_IS_PLAYING = "is_playing" const val COLUMN_PLAYING_SPEED_INT = "playing_speed_int" const val COLUMN_PLAYING_PITCH_INT = "playing_pitch_int" const val COLUMN_PLAYING_TIMESTAMP = "playing_timestamp" @@ -83,6 +97,7 @@ data class SongQueue( const val COLUMN_SPEED_INT = "speed_int" const val COLUMN_PITCH_INT = "pitch_int" const val COLUMN_PAUSE_ON_SONG_END = "pause_on_song_end" + const val COLUMN_SLEEP_TIMER_ENDS_AT = "sleep_timer_ends_at" const val SPEED_MULTIPLIER = 100 const val PITCH_MULTIPLIER = 100 diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueueSongMapping.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueueSongMapping.kt index 03d1d86b..60e0922a 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueueSongMapping.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueueSongMapping.kt @@ -29,18 +29,11 @@ import androidx.room.PrimaryKey childColumns = arrayOf(SongQueueSongMapping.COLUMN_NEXT_ID), onDelete = ForeignKey.SET_NULL, ), - ForeignKey( - entity = SongQueueSongMapping::class, - parentColumns = arrayOf(SongQueueSongMapping.COLUMN_ID), - childColumns = arrayOf(SongQueueSongMapping.COLUMN_OG_NEXT_ID), - onDelete = ForeignKey.SET_NULL, - ), ], indices = [ Index(SongQueueSongMapping.COLUMN_QUEUE_ID), Index(SongQueueSongMapping.COLUMN_IS_HEAD), Index(SongQueueSongMapping.COLUMN_NEXT_ID), - Index(SongQueueSongMapping.COLUMN_OG_NEXT_ID), ], ) data class SongQueueSongMapping( @@ -55,8 +48,6 @@ data class SongQueueSongMapping( val isHead: Boolean, @ColumnInfo(COLUMN_NEXT_ID) val nextId: String?, - @ColumnInfo(COLUMN_OG_NEXT_ID) - val ogNextId: String?, ) { companion object { const val TABLE = "song_queue_songs_mapping" @@ -65,6 +56,5 @@ data class SongQueueSongMapping( const val COLUMN_SONG_ID = "song_id" const val COLUMN_IS_HEAD = "is_head" const val COLUMN_NEXT_ID = "next_id" - const val COLUMN_OG_NEXT_ID = "og_next_id" } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/radio/Radio.kt b/app/src/main/java/io/github/zyrouge/symphony/services/radio/Radio.kt index f2e2d8b4..24f14f65 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/radio/Radio.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/radio/Radio.kt @@ -1,10 +1,10 @@ package io.github.zyrouge.symphony.services.radio +import android.net.Uri import io.github.zyrouge.symphony.Symphony import io.github.zyrouge.symphony.SymphonyHooks -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update +import io.github.zyrouge.symphony.services.groove.entities.Song +import io.github.zyrouge.symphony.services.groove.entities.SongQueue import kotlinx.coroutines.launch import java.time.Instant import java.util.Date @@ -12,25 +12,14 @@ import java.util.Timer class Radio(private val symphony: Symphony) : SymphonyHooks { data class SleepTimer( - val duration: Long, val endsAt: Long, val quitOnEnd: Boolean, + val timer: Timer, ) private val queue = RadioQueue(symphony) private val player = RadioPlayer(symphony) - - private var _sleepTimerTimer: Timer? = null - private val _sleepTimer = MutableStateFlow(null) - val sleepTimer = _sleepTimer.asStateFlow() - private val _pauseOnCurrentSongEnd = MutableStateFlow(false) - val pauseOnCurrentSongEnd = _pauseOnCurrentSongEnd.asStateFlow() - - val isPlaying get() = player.isPlaying - val playbackPosition get() = player.playbackPosition - val volume get() = player.volume - val speed get() = player.speed - val pitch get() = player.pitch + private var sleepTimer: SleepTimer? = null suspend fun play(): Boolean { if (player.hasMedia()) { @@ -38,91 +27,246 @@ class Radio(private val symphony: Symphony) : SymphonyHooks { return true } val songQueue = queue.getCurrentSongQueue() ?: return false - val song = symphony.database.songQueueSongMapping.findHead(songQueue.entity.id) + val songQueueId = songQueue.entity.id + var song: Song.AlongSongQueueMapping? = null + var playingTimestamp: Long? = null + if (songQueue.entity.playingId != null) { + song = symphony.database.songQueueSongMapping.findById( + songQueueId, + songQueue.entity.playingId + ) + playingTimestamp = songQueue.entity.playingTimestamp + } + if (song == null) { + song = symphony.database.songQueueSongMapping.findHead(songQueue.entity.id) + playingTimestamp = null + } + if (song == null) { + return false + } + play(song.mapping.id, song.entity.uri, playingTimestamp ?: 0L) + return true + } + + suspend fun play(songMappingId: String): Boolean { + player.stop() + val songQueue = queue.getCurrentSongQueue() ?: return false + val songQueueId = songQueue.entity.id + val song = symphony.database.songQueueSongMapping.findById(songQueueId, songMappingId) ?: return false - player.setMedia(song.entity.uri) - player.play() + play(songMappingId, song.entity.uri) return true } - suspend fun pause() { + private suspend fun play( + songMappingId: String, + songUri: Uri, + seek: Long = 0, + speed: Float = 1f, + pitch: Float = 1f, + ) { + if (player.getMedia() == songUri) { + player.play() + return + } + queue.updateCurrentSongQueue { + it.entity.copy( + playingId = songMappingId, + isPlaying = false, + playingTimestamp = seek, + playingSpeedInt = (speed * SongQueue.SPEED_MULTIPLIER).toInt(), + playingPitchInt = (pitch * SongQueue.PITCH_MULTIPLIER).toInt(), + ) + } + val media = RadioPlayer.PlayableMedia(songMappingId, songUri) + player.setMedia(media) + player.seek(seek) + player.setSpeed(speed) + player.setPitch(pitch) + player.play() + return + } + + suspend fun pause(): Boolean { player.pause() + return true } - suspend fun stop() { + suspend fun stop(): Boolean { player.stop() queue.clear() + return true } - fun jumpTo(index: Int) = play(PlayOptions(index = index)) - fun jumpToPrevious() = jumpTo(queue.currentSongIndex - 1) - fun jumpToNext() = jumpTo(queue.currentSongIndex + 1) - fun canJumpToPrevious() = queue.hasSongAt(queue.currentSongIndex - 1) - fun canJumpToNext() = queue.hasSongAt(queue.currentSongIndex + 1) - - fun seek(position: Long) { - player?.let { - it.seek(position.toInt()) - onUpdate.dispatch(Events.Player.Seeked) - } + suspend fun seek(duration: Long): Boolean { + player.seek(duration) + return true } - suspend fun setSpeed(speed: Float, persist: Boolean) { - // TODO: implement persist + suspend fun setSpeed(speed: Float, persist: Boolean): Boolean { + val success = queue.updateCurrentSongQueue { + val speedInt = (speed * SongQueue.SPEED_MULTIPLIER).toInt() + val persistedSpeedInt = if (persist) speedInt else it.entity.speedInt + it.entity.copy(playingSpeedInt = speedInt, speedInt = persistedSpeedInt) + } + if (!success) { + return false + } player.setSpeed(speed) + return true } - suspend fun setPitch(pitch: Float, persist: Boolean) { - // TODO: implement persist + suspend fun setPitch(pitch: Float, persist: Boolean): Boolean { + val success = queue.updateCurrentSongQueue { + val pitchInt = (pitch * SongQueue.PITCH_MULTIPLIER).toInt() + val persistedPitchInt = if (persist) pitchInt else it.entity.pitchInt + it.entity.copy(playingPitchInt = pitchInt, pitchInt = persistedPitchInt) + } + if (!success) { + return false + } player.setPitch(pitch) + return true } - fun setSleepTimer(duration: Long, quitOnEnd: Boolean = false) { - val endsAt = System.currentTimeMillis() + duration - val sleepTimer = SleepTimer(duration = duration, endsAt = endsAt, quitOnEnd = quitOnEnd) + suspend fun setSleepTimer(endsAt: Long?): Boolean { + val success = queue.updateCurrentSongQueue { + it.entity.copy(sleepTimerEndsAt = null) + } + if (!success) { + return false + } + cancelSleepTimer() + if (endsAt == null) { + return true + } + val quitOnEnd = false + val timerTask = kotlin.concurrent.timerTask { + val shouldQuit = quitOnEnd + cancelSleepTimer() + symphony.groove.coroutineScope.launch { + pause() + } + if (shouldQuit) { + symphony.closeApp?.invoke() + } + } val timer = Timer() - timer.schedule( - kotlin.concurrent.timerTask { - val shouldQuit = sleepTimer.quitOnEnd - clearSleepTimer() - symphony.groove.coroutineScope.launch { - pause() - } - if (shouldQuit) { - symphony.closeApp?.invoke() - } - }, - Date.from(Instant.ofEpochMilli(endsAt)), - ) - clearSleepTimer() - _sleepTimerTimer = timer - _sleepTimer.update { sleepTimer } + timer.schedule(timerTask, Date.from(Instant.ofEpochMilli(endsAt))) + cancelSleepTimer() + sleepTimer = SleepTimer(endsAt = endsAt, quitOnEnd = quitOnEnd, timer = timer) + return true + } + + private fun cancelSleepTimer() { + sleepTimer?.timer?.cancel() + sleepTimer = null + } + + suspend fun setPauseOnCurrentSongEnd(value: Boolean) = queue.updateCurrentSongQueue { + it.entity.copy(pauseOnSongEnd = value) } - fun clearSleepTimer() { - _sleepTimerTimer?.cancel() - _sleepTimerTimer = null - _sleepTimer.update { null } + suspend fun toggleLoopMode() { + queue.toggleLoopMode() } - fun setPauseOnCurrentSongEnd(value: Boolean) { - _pauseOnCurrentSongEnd.update { value } + suspend fun setLoopMode(loopMode: SongQueue.LoopMode) { + queue.setLoopMode(loopMode) } - private fun onCurrentSongEnded() { + suspend fun toggleShuffleMode() { + queue.toggleShuffleMode() + } + + suspend fun setShuffleMode(to: Boolean) { + queue.setShuffleMode(to) + } + + suspend fun add( + songIds: List, + position: RadioQueue.AddPosition = RadioQueue.AddPosition.AfterTail, + ) = queue.add(songIds, position) + + suspend fun add( + songId: String, + position: RadioQueue.AddPosition = RadioQueue.AddPosition.AfterTail, + ) = queue.add(songId, position) + + suspend fun add( + songs: List, + position: RadioQueue.AddPosition = RadioQueue.AddPosition.AfterTail, + ) = queue.add(songs, position) + + suspend fun add( + song: Song, + position: RadioQueue.AddPosition = RadioQueue.AddPosition.AfterTail, + ) = queue.add(song, position) + + suspend fun remove(songMappingIds: List) = queue.remove(songMappingIds) + + suspend fun remove(songMappingId: String) = queue.remove(songMappingId) + + internal fun onCurrentSongEnded() { } - private fun onNextSongChange() { + internal fun onNextSongChange() { } - private enum class SongEndedReason { + internal enum class SongEndedReason { Finish, Exception, } - private fun onSongEnded(source: SongEndedReason) { + internal fun onPlayerSongEnded(source: SongEndedReason) { + symphony.groove.coroutineScope.launch { + onPlayerSongEndedNeedsSuspend(source) + } + } + + private suspend fun onPlayerSongEndedNeedsSuspend(source: SongEndedReason) { + val songQueue = queue.getCurrentSongQueue() ?: return + val queueId = songQueue.entity.id + val media = player.getMedia() ?: return + val song = symphony.database.songQueueSongMapping.findById(queueId, media.id) ?: return + val nextSongMappingId = song.mapping.nextId ?: return + val nextSong = + symphony.database.songQueueSongMapping.findById(queueId, nextSongMappingId) ?: return + val nextMedia = RadioPlayer.PlayableMedia(nextSongMappingId, nextSong.entity.uri) + player.setNextMedia(nextMedia) + } + + internal fun onPlayerIsPlayingChanged(isPlaying: Boolean) { + symphony.groove.coroutineScope.launch { + onPlayerIsPlayingChangedNeedsSuspend(isPlaying) + } + } + + private suspend fun onPlayerIsPlayingChangedNeedsSuspend(isPlaying: Boolean) { + queue.updateCurrentSongQueue { + if (it.entity.isPlaying == isPlaying) { + return@updateCurrentSongQueue null + } + it.entity.copy(isPlaying = isPlaying) + } + } + + internal fun onPlayerPlaybackParametersChanged(speed: Float, pitch: Float) { + symphony.groove.coroutineScope.launch { + onPlayerPlaybackParametersChangedNeedsSuspend(speed, pitch) + } + } + private suspend fun onPlayerPlaybackParametersChangedNeedsSuspend(speed: Float, pitch: Float) { + queue.updateCurrentSongQueue { + val speedInt = (speed * SongQueue.SPEED_MULTIPLIER).toInt() + val pitchInt = (pitch * SongQueue.PITCH_MULTIPLIER).toInt() + if (it.entity.speedInt == speedInt || it.entity.pitchInt == pitchInt) { + return@updateCurrentSongQueue null + } + it.entity.copy(speedInt = speedInt, pitchInt = pitchInt) + } } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioPlayer.kt b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioPlayer.kt index aa041e5b..4e5bec43 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioPlayer.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioPlayer.kt @@ -14,15 +14,23 @@ import androidx.media3.exoplayer.ExoPlayer import androidx.media3.session.MediaSession import io.github.zyrouge.symphony.Symphony import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import java.util.Timer @OptIn(UnstableApi::class) class RadioPlayer(val symphony: Symphony) { + data class PlayableMedia(val id: String, val uri: Uri) { + fun toMediaItem() = MediaItem.Builder().setMediaId(id).setUri(uri).build() + + companion object { + fun fromMediaItem(mediaItem: MediaItem): PlayableMedia { + val uri = mediaItem.localConfiguration?.uri + ?: throw Exception("Missing media item uri") + return PlayableMedia(mediaItem.mediaId, uri) + } + } + } + data class PlaybackPosition(val played: Long, val total: Long) { val ratio: Float get() = (played.toFloat() / total).takeIf { it.isFinite() } ?: 0f @@ -77,18 +85,6 @@ class RadioPlayer(val symphony: Symphony) { // val fadePlayback get() = symphony.settings.fadePlayback.value - private val _isPlaying = MutableStateFlow(false) - val isPlaying = _isPlaying.asStateFlow() - private var playbackPositionUpdater: Timer? = null - private val _playbackPosition = MutableStateFlow(PlaybackPosition.zero) - val playbackPosition = _playbackPosition.asStateFlow() - private val _volume = MutableStateFlow(MAX_VOLUME) - val volume = _volume.asStateFlow() - private val _speed = MutableStateFlow(DEFAULT_SPEED) - val speed = _speed.asStateFlow() - private val _pitch = MutableStateFlow(DEFAULT_PITCH) - val pitch = _pitch.asStateFlow() - init { symphony.groove.coroutineScope.launch { prepare() @@ -102,14 +98,28 @@ class RadioPlayer(val symphony: Symphony) { suspend fun hasMedia() = withMediaPlayer { it.currentMediaItem != null } - suspend fun hasNextMedia() = withMediaPlayer { it.getMediaItemAt(it.currentMediaItemIndex + 1) } + suspend fun hasNextMedia() = withMediaPlayer { + it.currentMediaItemIndex + 1 < it.mediaItemCount + } + + suspend fun getMedia() = withMediaPlayer { + it.currentMediaItem?.let { mediaItem -> PlayableMedia.fromMediaItem(mediaItem) } + } + + suspend fun getNextMedia() = withMediaPlayer { + val nextIndex = it.currentMediaItemIndex + 1 + when { + nextIndex < it.mediaItemCount -> PlayableMedia.fromMediaItem(it.getMediaItemAt(nextIndex)) + else -> null + } + } - suspend fun setMedia(uri: Uri) = withMediaPlayer { - it.setMediaItems(listOf(MediaItem.fromUri(uri))) + suspend fun setMedia(media: PlayableMedia) = withMediaPlayer { + it.setMediaItems(listOf(media.toMediaItem())) } - suspend fun setNextMedia(uri: Uri) = withMediaPlayer { - it.replaceMediaItem(1, MediaItem.fromUri(uri)) + suspend fun setNextMedia(media: PlayableMedia) = withMediaPlayer { + it.replaceMediaItem(1, media.toMediaItem()) it.play() } @@ -142,25 +152,6 @@ class RadioPlayer(val symphony: Symphony) { it.playbackParameters = it.playbackParameters.withPitch(to) } - private fun createDurationTimer() { - playbackPositionUpdater = kotlin.concurrent.timer(period = 500L) { - emitPlaybackPosition() - } - } - - private fun emitPlaybackPosition() { - symphony.groove.coroutineScope.launch(Dispatchers.Main) { - _playbackPosition.update { - PlaybackPosition(mediaPlayerUnsafe.currentPosition, mediaPlayerUnsafe.duration) - } - } - } - - private fun destroyDurationTimer() { - playbackPositionUpdater?.cancel() - playbackPositionUpdater = null - } - private suspend fun withMediaPlayer(fn: (ExoPlayer) -> T): T { return withContext(Dispatchers.Main) { fn(mediaPlayerUnsafe) @@ -174,20 +165,17 @@ class RadioPlayer(val symphony: Symphony) { } fun onMediaPlayerIsPlayingChanged(isPlaying: Boolean) { - when { - isPlaying -> createDurationTimer() - else -> destroyDurationTimer() - } - _isPlaying.update { isPlaying } + symphony.radio.onPlayerIsPlayingChanged(isPlaying) } fun onMediaPlayerVolumeChanged(volume: Float) { - _volume.update { volume } } fun onMediaPlayerPlaybackParametersChanged(playbackParameters: PlaybackParameters) { - _speed.update { playbackParameters.speed } - _pitch.update { playbackParameters.pitch } + symphony.radio.onPlayerPlaybackParametersChanged( + speed = playbackParameters.speed, + pitch = playbackParameters.pitch, + ) } fun onMediaPlayerPlaybackStateChanged(playbackState: Int) { @@ -201,14 +189,13 @@ class RadioPlayer(val symphony: Symphony) { } private fun onMediaPlayerMediaEnded() { - emitPlaybackPosition() withMediaPlayerNoSuspend { - // keep playing item at 0 if (it.mediaItemCount == 0) { return@withMediaPlayerNoSuspend } it.removeMediaItem(0) } + symphony.radio.onPlayerSongEnded(Radio.SongEndedReason.Finish) } companion object { diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioQueue.kt b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioQueue.kt index 7e4990af..e2a98747 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioQueue.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioQueue.kt @@ -1,15 +1,17 @@ package io.github.zyrouge.symphony.services.radio +import androidx.room.withTransaction import io.github.zyrouge.symphony.Symphony +import io.github.zyrouge.symphony.services.database.PersistentDatabase import io.github.zyrouge.symphony.services.database.store.SongQueueSongMappingStore import io.github.zyrouge.symphony.services.groove.entities.Song import io.github.zyrouge.symphony.services.groove.entities.SongQueue import io.github.zyrouge.symphony.services.groove.entities.SongQueueSongMapping -import io.github.zyrouge.symphony.utils.lazy_linked_list.LazyLinkedListOperatorHelper +import io.github.zyrouge.symphony.utils.lazy_linked_list.LazyLinkedListOperator class RadioQueue(private val symphony: Symphony) { private class SongQueueSongMappingOperatorEntityFunctions : - LazyLinkedListOperatorHelper.EntityFunctions { + LazyLinkedListOperator.EntityFunctions { override fun getEntityId(entity: SongQueueSongMapping) = entity.id override fun getEntityNextId(entity: SongQueueSongMapping) = entity.nextId override fun getEntityIsHead(entity: SongQueueSongMapping) = entity.isHead @@ -22,10 +24,11 @@ class RadioQueue(private val symphony: Symphony) { } private class SongQueueSongMappingOperatorPersistenceFunctions( + private val persistentDatabase: PersistentDatabase, private val store: SongQueueSongMappingStore, private val queueId: String, ) : - LazyLinkedListOperatorHelper.PersistenceFunctions { + LazyLinkedListOperator.PersistenceFunctions { override fun getEntitiesByIds(ids: List) = store.entriesByIds(queueId, ids) .mapValues { it.value.mapping } @@ -36,19 +39,31 @@ class RadioQueue(private val symphony: Symphony) { override fun getHeadEntity() = store.findHead(queueId)?.mapping override fun getTailEntity() = store.findByNextId(queueId, null)?.mapping - override suspend fun insertEntities(entities: List) { - store.insert(*entities.toTypedArray()) - } - - override suspend fun updateEntities(entities: List) { - store.update(*entities.toTypedArray()) - } - - override suspend fun deleteEntities(ids: List) { - store.delete(queueId, ids) + override suspend fun saveEntities( + addedEntities: List, + modifiedEntities: List, + deletedKeys: List, + ) { + if (addedEntities.isEmpty() || modifiedEntities.isEmpty() || deletedKeys.isEmpty()) { + return + } + persistentDatabase.withTransaction { + if (addedEntities.isNotEmpty()) { + store.insert(*addedEntities.toTypedArray()) + } + if (modifiedEntities.isNotEmpty()) { + store.update(*modifiedEntities.toTypedArray()) + } + if (deletedKeys.isNotEmpty()) { + store.delete(queueId, deletedKeys) + } + } } } + private interface SongQueueSongMappingOperatorDataChangeFunctions : + LazyLinkedListOperator.DataChangeFunctions + // val queueFlow = symphony.database.songQueue.findFirstAsFlow() // val songQueue = AtomicReference(null) @@ -75,60 +90,106 @@ class RadioQueue(private val symphony: Symphony) { // } } - suspend fun add(songIds: List, insertAfterId: String? = null) { - val origQueue = getCurrentSongQueue() - val songQueueId = origQueue?.entity?.id ?: symphony.database.songQueueIdGenerator.next() - if (origQueue == null) { - val songQueue = SongQueue( - id = songQueueId, - playingId = null, - playingTimestamp = null, - playingSpeedInt = SongQueue.SPEED_MULTIPLIER, - playingPitchInt = SongQueue.PITCH_MULTIPLIER, - shuffled = false, - loopMode = SongQueue.LoopMode.None, - speedInt = SongQueue.SPEED_MULTIPLIER, - pitchInt = SongQueue.PITCH_MULTIPLIER, - pauseOnSongEnd = false, - ) - symphony.database.songQueue.insert(songQueue) - } + sealed class AddPosition { + object BeforeHead : AddPosition() + class After(val id: String) : AddPosition() + object AfterTail : AddPosition() + } + + private suspend fun add( + songQueue: SongQueue, + songIds: List, + position: AddPosition = AddPosition.AfterTail, + ): Boolean { + val songQueueId = songQueue.id val operator = createSongQueueSongMappingOperator(songQueueId) - operator.append(insertAfterId, songIds) { x, isHead, nextId -> - SongQueueSongMapping( - id = symphony.database.songQueueSongMappingIdGenerator.next(), - queueId = songQueueId, - songId = x, - isHead = isHead, - nextId = nextId, - ogNextId = nextId, - ) + val changeset = when (position) { + AddPosition.BeforeHead -> operator.prependHead(songIds) { x, isHead, nextId -> + SongQueueSongMapping( + id = symphony.database.songQueueSongMappingIdGenerator.next(), + queueId = songQueueId, + songId = x, + isHead = isHead, + nextId = nextId, + ) + } + + is AddPosition.After, AddPosition.AfterTail -> { + val insertAfterId = if (position is AddPosition.After) position.id else null + operator.append(insertAfterId, songIds) { x, isHead, nextId -> + SongQueueSongMapping( + id = symphony.database.songQueueSongMappingIdGenerator.next(), + queueId = songQueueId, + songId = x, + isHead = isHead, + nextId = nextId, + ) + } + } + } + operator.persist(changeset) + songQueue.originalId?.let { originalSongQueueId -> + val originalSongQueue = + symphony.database.songQueue.findById(originalSongQueueId) ?: return@let + // append at end for now + add(originalSongQueue.entity, songIds, AddPosition.AfterTail) } + return changeset.addedKeys.isNotEmpty() } - suspend fun add(songId: String, insertAfterId: String? = null) { - add(listOf(songId), insertAfterId) + suspend fun add(songIds: List, position: AddPosition = AddPosition.AfterTail): Boolean { + val songQueue = createOrGetCurrentSongQueue() + return add(songQueue, songIds, position) } - suspend fun add(songs: List, insertAfterId: String? = null) { - add(songs.map { it.id }, insertAfterId) - } + suspend fun add(songId: String, position: AddPosition = AddPosition.AfterTail) = + add(listOf(songId), position) - suspend fun add(song: Song, insertAfterId: String? = null) { - add(listOf(song.id), insertAfterId) + suspend fun add(songs: List, position: AddPosition = AddPosition.AfterTail) = + add(songs.map { it.id }, position) + + suspend fun add(song: Song, position: AddPosition = AddPosition.AfterTail) = + add(listOf(song.id), position) + + suspend fun remove(songQueue: SongQueue, songMappingIds: List): Boolean { + val songQueueId = songQueue.id + val playingId = songQueue.playingId + var nPlayingId = playingId + val deletedSongIds = mutableListOf() + val operator = createSongQueueSongMappingOperator( + songQueueId, + object : SongQueueSongMappingOperatorDataChangeFunctions { + override fun onMarkedAsDeleted(key: String, value: SongQueueSongMapping) { + if (key == nPlayingId) { + nPlayingId = value.nextId + } + value.songId?.let { deletedSongIds.add(it) } + } + } + ) + val changeset = operator.remove(songMappingIds) + if (playingId != nPlayingId) { + val nSongQueue = songQueue.copy(playingId = nPlayingId) + symphony.database.songQueue.update(nSongQueue) + } + operator.persist(changeset) + songQueue.originalId?.let { originalSongQueueId -> + val originalSongQueue = + symphony.database.songQueue.findById(originalSongQueueId) ?: return@let + val originalSongQueueSongMappingIds = symphony.database.songQueueSongMapping + .entriesBySongIds(originalSongQueueId, deletedSongIds).values + .map { it.mapping.id } + remove(originalSongQueue.entity, originalSongQueueSongMappingIds) + } + return changeset.deletedKeys.isNotEmpty() } suspend fun remove(songMappingIds: List): Boolean { val songQueue = getCurrentSongQueue() ?: return false - val songQueueId = songQueue.entity.id - val operator = createSongQueueSongMappingOperator(songQueueId) - val result = operator.remove(songMappingIds) - return result.deletedKeys.isNotEmpty() + return remove(songQueue.entity, songMappingIds) } - suspend fun remove(songMappingId: String) { - remove(listOf(songMappingId)) - } + suspend fun remove(songMappingId: String) = remove(listOf(songMappingId)) suspend fun clear(): Boolean { val songQueue = getCurrentSongQueue() ?: return false @@ -136,63 +197,144 @@ class RadioQueue(private val symphony: Symphony) { return true } - private suspend fun setLoopMode(songQueue: SongQueue, loopMode: SongQueue.LoopMode) { - val nSongQueue = songQueue.copy(loopMode = loopMode) - symphony.database.songQueue.update(nSongQueue) + suspend fun toggleLoopMode() = updateCurrentSongQueue { + val currentLoopMode = it.entity.loopMode + val nextLoopModeOrdinal = (currentLoopMode.ordinal + 1) % SongQueue.LoopMode.values.size + val nextLoopMode = SongQueue.LoopMode.values[nextLoopModeOrdinal] + it.entity.copy(loopMode = nextLoopMode) + } + + suspend fun setLoopMode(loopMode: SongQueue.LoopMode) = updateCurrentSongQueue { + it.entity.copy(loopMode = loopMode) } - suspend fun setLoopMode(loopMode: SongQueue.LoopMode): Boolean { + suspend fun toggleShuffleMode(): Boolean { val songQueue = getCurrentSongQueue() ?: return false - setLoopMode(songQueue.entity, loopMode) - return true + return setShuffleMode(songQueue.entity, !songQueue.entity.shuffled) } - suspend fun toggleLoopMode(): Boolean { + suspend fun setShuffleMode(to: Boolean): Boolean { val songQueue = getCurrentSongQueue() ?: return false - val currentLoopMode = songQueue.entity.loopMode - val nextLoopModeOrdinal = (currentLoopMode.ordinal + 1) % SongQueue.LoopMode.values.size - val nextLoopMode = SongQueue.LoopMode.values[nextLoopModeOrdinal] - setLoopMode(songQueue.entity, nextLoopMode) + return setShuffleMode(songQueue.entity, to) + } + + private suspend fun setShuffleMode(songQueue: SongQueue, to: Boolean): Boolean { + if (songQueue.shuffled == to) { + return true + } + if (songQueue.shuffled) { + return deleteShuffledQueue(songQueue) + } + return createShuffledQueue(songQueue) + } + + private suspend fun createShuffledQueue(originalSongQueue: SongQueue): Boolean { + val originalSongQueueId = originalSongQueue.id + val nOriginalSongQueue = originalSongQueue.copy(internalId = null) + val shuffledSongQueueId = symphony.database.songQueueIdGenerator.next() + val songs = symphony.database.songQueueSongMapping.values(originalSongQueueId) + val shuffledSongs = mutableListOf() + var shuffledSongQueuePlayingId: String? = null + var shuffledSongNextId: String? = null + var i = 0 + for (x in songs.shuffled()) { + val shuffledSongId = symphony.database.songQueueSongMappingIdGenerator.next() + val shuffledSong = SongQueueSongMapping( + id = shuffledSongId, + queueId = shuffledSongQueueId, + songId = x.mapping.songId, + isHead = i == songs.size - 1, + nextId = shuffledSongNextId, + ) + shuffledSongs.add(shuffledSong) + if (originalSongQueue.playingId == x.mapping.id) { + shuffledSongQueuePlayingId = shuffledSongId + } + shuffledSongNextId = shuffledSongId + i++ + } + val shuffledSongQueue = originalSongQueue.copy( + id = shuffledSongQueueId, + originalId = originalSongQueueId, + internalId = SONG_QUEUE_INTERNAL_ID_DEFAULT, + playingId = shuffledSongQueuePlayingId, + shuffled = true, + ) + symphony.database.persistent.withTransaction { + symphony.database.songQueue.update(nOriginalSongQueue) + symphony.database.songQueue.insert(shuffledSongQueue) + symphony.database.songQueueSongMapping.insert(*shuffledSongs.toTypedArray()) + } return true } - fun toggleShuffleMode(): Boolean { -// val songQueue = getSongQueue() ?: return false -// val nQueue = queue.entity.copy(shuffled = !queue.entity.shuffled) -// symphony.database.songQueue.update(nQueue) + private suspend fun deleteShuffledQueue(shuffledSongQueue: SongQueue): Boolean { + val originalSongQueueId = shuffledSongQueue.originalId ?: return false + val shuffledSongQueueId = shuffledSongQueue.id + val nShuffledSongQueue = shuffledSongQueue.copy(internalId = null) + val nOriginalSongQueuePlayingId = shuffledSongQueue.playingId + ?.let { symphony.database.songQueueSongMapping.findById(shuffledSongQueueId, it) } + ?.mapping?.songId + ?.let { symphony.database.songQueueSongMapping.findBySongId(originalSongQueueId, it) } + ?.mapping?.id + val nOriginalSongQueue = shuffledSongQueue.copy( + id = originalSongQueueId, + internalId = SONG_QUEUE_INTERNAL_ID_DEFAULT, + playingId = nOriginalSongQueuePlayingId, + shuffled = false, + ) + symphony.database.persistent.withTransaction { + symphony.database.songQueue.update(nShuffledSongQueue) + symphony.database.songQueue.insert(nOriginalSongQueue) + symphony.database.songQueue.delete(shuffledSongQueueId) + } return true } - fun setShuffleMode(to: Boolean) { -// currentShuffleMode = to -// if (currentQueue.isNotEmpty()) { -// val currentSongId = getSongIdAt(currentSongIndex) ?: getSongIdAt(0)!! -// currentSongIndex = if (currentShuffleMode) { -// val newQueue = originalQueue.toMutableList() -// newQueue.removeAt(currentSongIndex) -// newQueue.shuffle() -// newQueue.add(0, currentSongId) -// currentQueue.clear() -// currentQueue.addAll(newQueue) -// 0 -// } else { -// currentQueue.clear() -// currentQueue.addAll(originalQueue) -// originalQueue.indexOfFirst { it == currentSongId } -// } -// } -// symphony.radio.onUpdate.dispatch(Radio.Events.Queue.Modified) + internal suspend fun updateCurrentSongQueue(fn: (SongQueue.AlongAttributes) -> SongQueue?): Boolean { + val songQueue = getCurrentSongQueue() ?: return false + val nSongQueue = fn(songQueue) ?: return false + symphony.database.songQueue.update(nSongQueue) + return true } fun getCurrentSongQueue() = symphony.database.songQueue.findByInternalId(SONG_QUEUE_INTERNAL_ID_DEFAULT) - private fun createSongQueueSongMappingOperator(queueId: String) = LazyLinkedListOperatorHelper( + private suspend fun createOrGetCurrentSongQueue(): SongQueue { + getCurrentSongQueue()?.let { + return it.entity + } + val songQueue = SongQueue( + id = symphony.database.songQueueIdGenerator.next(), + internalId = SONG_QUEUE_INTERNAL_ID_DEFAULT, + playingId = null, + isPlaying = false, + playingTimestamp = 0, + playingSpeedInt = SongQueue.SPEED_MULTIPLIER, + playingPitchInt = SongQueue.PITCH_MULTIPLIER, + shuffled = false, + loopMode = SongQueue.LoopMode.None, + speedInt = SongQueue.SPEED_MULTIPLIER, + pitchInt = SongQueue.PITCH_MULTIPLIER, + pauseOnSongEnd = false, + sleepTimerEndsAt = null, + ) + symphony.database.songQueue.insert(songQueue) + return songQueue + } + + private fun createSongQueueSongMappingOperator( + queueId: String, + dataChangeFunctions: SongQueueSongMappingOperatorDataChangeFunctions? = null, + ) = LazyLinkedListOperator( SongQueueSongMappingOperatorEntityFunctions(), SongQueueSongMappingOperatorPersistenceFunctions( + symphony.database.persistent, symphony.database.songQueueSongMapping, - queueId - ) + queueId, + ), + dataChangeFunctions, ) companion object { diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListAppendOperation.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListAppendOperation.kt new file mode 100644 index 00000000..c4952c80 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListAppendOperation.kt @@ -0,0 +1,46 @@ +package io.github.zyrouge.symphony.utils.lazy_linked_list + +typealias LazyLinkedListInsertAppendOperationCreateFn = (value: X, isHead: Boolean, nextId: K?) -> V + +class LazyLinkedListInsertAppendOperation( + val operator: LazyLinkedListOperator, + val insertAfterId: K?, + val values: List, + val createFn: LazyLinkedListInsertAppendOperationCreateFn, +) : LazyLinkedListOperator.Operation { + override suspend fun perform(): LazyLinkedListOperator.Changeset { + if (values.isEmpty()) { + return LazyLinkedListOperator.Changeset() + } + val addedKeys = mutableListOf() + val added = mutableListOf() + val modifiedKeys = mutableListOf() + val modified = mutableListOf() + val insertAfterEntity = insertAfterId?.let { operator.persistenceFunctions.getEntity(it) } + ?: operator.persistenceFunctions.getTailEntity() + val hasHead = insertAfterEntity != null + var nextId = insertAfterEntity?.let { operator.entityFunctions.getEntityNextId(it) } + for (i in (values.size - 1) downTo 0) { + val value = values[i] + val isHead = i == 0 && !hasHead + val entity = createFn(value, isHead, nextId) + val id = operator.entityFunctions.getEntityId(entity) + addedKeys.add(id) + added.add(entity) + nextId = id + } + if (insertAfterEntity != null) { + val insertAfterId = operator.entityFunctions.getEntityId(insertAfterEntity) + val nInsertAfterEntity = + operator.entityFunctions.updateEntityNextId(insertAfterEntity, nextId) + modifiedKeys.add(insertAfterId) + modified.add(nInsertAfterEntity) + } + return LazyLinkedListOperator.Changeset( + addedKeys = addedKeys, + addedEntities = added, + modifiedKeys = modifiedKeys, + modifiedEntities = modified, + ) + } +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListAppendOperator.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListAppendOperator.kt deleted file mode 100644 index f7c60bb8..00000000 --- a/app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListAppendOperator.kt +++ /dev/null @@ -1,50 +0,0 @@ -package io.github.zyrouge.symphony.utils.lazy_linked_list - -typealias LazyLinkedListInsertAppendOperatorCreateFn = (value: X, isHead: Boolean, nextId: K?) -> V - -class LazyLinkedListInsertAppendOperator( - val helper: LazyLinkedListOperatorHelper, - val insertAfterId: K?, - val values: List, - val createFn: LazyLinkedListInsertAppendOperatorCreateFn, -) { - suspend fun operate(): LazyLinkedListOperatorHelper.Result { - if (values.isEmpty()) { - return LazyLinkedListOperatorHelper.Result() - } - val addedKeys = mutableListOf() - val added = mutableListOf() - val modifiedKeys = mutableListOf() - val modified = mutableListOf() - val insertAfterEntity = insertAfterId?.let { helper.persistenceFunctions.getEntity(it) } - ?: helper.persistenceFunctions.getTailEntity() - val hasHead = insertAfterEntity != null - var nextId = insertAfterEntity?.let { helper.entityFunctions.getEntityNextId(it) } - for (i in (values.size - 1) downTo 0) { - val value = values[i] - val isHead = i == 0 && !hasHead - val entity = createFn(value, isHead, nextId) - val id = helper.entityFunctions.getEntityId(entity) - addedKeys.add(id) - added.add(entity) - nextId = id - } - if (insertAfterEntity != null) { - val insertAfterId = helper.entityFunctions.getEntityId(insertAfterEntity) - val nInsertAfterEntity = - helper.entityFunctions.updateEntityNextId(insertAfterEntity, nextId) - modifiedKeys.add(insertAfterId) - modified.add(nInsertAfterEntity) - } - if (added.isNotEmpty()) { - helper.persistenceFunctions.insertEntities(added) - } - if (modified.isNotEmpty()) { - helper.persistenceFunctions.updateEntities(modified) - } - return LazyLinkedListOperatorHelper.Result( - addedKeys = addedKeys, - modifiedKeys = modifiedKeys - ) - } -} diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListOperatorHelper.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListOperator.kt similarity index 53% rename from app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListOperatorHelper.kt rename to app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListOperator.kt index 118b007a..525b8de0 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListOperatorHelper.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListOperator.kt @@ -1,8 +1,9 @@ package io.github.zyrouge.symphony.utils.lazy_linked_list -class LazyLinkedListOperatorHelper( +class LazyLinkedListOperator( val entityFunctions: EntityFunctions, val persistenceFunctions: PersistenceFunctions, + val dataChangeFunctions: DataChangeFunctions? = null, ) { interface EntityFunctions { fun getEntityId(entity: V): K @@ -19,28 +20,49 @@ class LazyLinkedListOperatorHelper( fun getEntityByNextId(nextId: K) = getEntitiesByIds(listOf(nextId))[nextId] fun getHeadEntity(): V? fun getTailEntity(): V? - suspend fun insertEntities(entities: List) - suspend fun updateEntities(entities: List) - suspend fun deleteEntities(ids: List) + + suspend fun saveEntities( + addedEntities: List, + modifiedEntities: List, + deletedKeys: List, + ) + } + + interface DataChangeFunctions { + fun onMarkedAsDeleted(key: K, value: V) {} } - data class Result( + internal interface Operation { + suspend fun perform(): Changeset + } + + data class Changeset( val headModified: Boolean = false, val addedKeys: List = emptyList(), + internal val addedEntities: List = emptyList(), val modifiedKeys: List = emptyList(), + internal val modifiedEntities: List = emptyList(), val deletedKeys: List = emptyList(), ) suspend fun prependHead( values: List, createFn: LazyLinkedListPrependHeadOperatorCreateFn, - ) = LazyLinkedListPrependMoveOperator(this, values, createFn).operate() + ) = LazyLinkedListPrependHeadOperation(this, values, createFn).perform() suspend fun append( insertAfterId: K?, values: List, - createFn: LazyLinkedListInsertAppendOperatorCreateFn, - ) = LazyLinkedListInsertAppendOperator(this, insertAfterId, values, createFn).operate() + createFn: LazyLinkedListInsertAppendOperationCreateFn, + ) = LazyLinkedListInsertAppendOperation(this, insertAfterId, values, createFn).perform() + + suspend fun remove(keys: List) = LazyLinkedListRemoveOperation(this, keys).perform() - suspend fun remove(keys: List) = LazyLinkedListRemoveOperator(this, keys).operate() -} \ No newline at end of file + suspend fun persist(changeset: Changeset) { + persistenceFunctions.saveEntities( + addedEntities = changeset.addedEntities, + modifiedEntities = changeset.modifiedEntities, + deletedKeys = changeset.deletedKeys, + ) + } +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListPrependHeadOperator.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListPrependHeadOperation.kt similarity index 50% rename from app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListPrependHeadOperator.kt rename to app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListPrependHeadOperation.kt index 4c8922cf..bc4938bd 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListPrependHeadOperator.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListPrependHeadOperation.kt @@ -2,46 +2,42 @@ package io.github.zyrouge.symphony.utils.lazy_linked_list typealias LazyLinkedListPrependHeadOperatorCreateFn = (value: X, isHead: Boolean, nextId: K?) -> V -class LazyLinkedListPrependHeadOperator( - val helper: LazyLinkedListOperatorHelper, +class LazyLinkedListPrependHeadOperation( + val operator: LazyLinkedListOperator, val values: List, val createFn: LazyLinkedListPrependHeadOperatorCreateFn, -) { - suspend fun operate(): LazyLinkedListOperatorHelper.Result { +) : LazyLinkedListOperator.Operation { + override suspend fun perform(): LazyLinkedListOperator.Changeset { if (values.isEmpty()) { - return LazyLinkedListOperatorHelper.Result() + return LazyLinkedListOperator.Changeset() } val addedKeys = mutableListOf() val added = mutableListOf() val modifiedKeys = mutableListOf() val modified = mutableListOf() - val headEntity = helper.persistenceFunctions.getHeadEntity() - var nextId = headEntity?.let { helper.entityFunctions.getEntityId(it) } + val headEntity = operator.persistenceFunctions.getHeadEntity() + var nextId = headEntity?.let { operator.entityFunctions.getEntityId(it) } for (i in (values.size - 1) downTo 0) { val value = values[i] val isHead = i == 0 val entity = createFn(value, isHead, nextId) - val id = helper.entityFunctions.getEntityId(entity) + val id = operator.entityFunctions.getEntityId(entity) addedKeys.add(id) added.add(entity) nextId = id } if (headEntity != null) { - val headEntityId = helper.entityFunctions.getEntityId(headEntity) - val nHeadEntity = helper.entityFunctions.updateEntityIsHead(headEntity, false) + val headEntityId = operator.entityFunctions.getEntityId(headEntity) + val nHeadEntity = operator.entityFunctions.updateEntityIsHead(headEntity, false) modifiedKeys.add(headEntityId) modified.add(nHeadEntity) } - if (added.isNotEmpty()) { - helper.persistenceFunctions.insertEntities(added) - } - if (modified.isNotEmpty()) { - helper.persistenceFunctions.updateEntities(modified) - } - return LazyLinkedListOperatorHelper.Result( + return LazyLinkedListOperator.Changeset( headModified = true, addedKeys = addedKeys, - modifiedKeys = modifiedKeys + addedEntities = added, + modifiedKeys = modifiedKeys, + modifiedEntities = modified, ) } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListRemoveOperation.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListRemoveOperation.kt new file mode 100644 index 00000000..a40cef1f --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListRemoveOperation.kt @@ -0,0 +1,63 @@ +package io.github.zyrouge.symphony.utils.lazy_linked_list + +class LazyLinkedListRemoveOperation( + val operator: LazyLinkedListOperator, + val keys: List, +) : LazyLinkedListOperator.Operation { + override suspend fun perform(): LazyLinkedListOperator.Changeset { + if (keys.isEmpty()) { + return LazyLinkedListOperator.Changeset() + } + val entities = operator.persistenceFunctions.getEntitiesByIds(keys).toMutableMap() + val idToPreviousId = mutableMapOf() + for (x in entities.values) { + val id = operator.entityFunctions.getEntityId(x) + val nextId = operator.entityFunctions.getEntityId(x) + idToPreviousId[nextId] = id + } + for (x in operator.persistenceFunctions.getEntitiesByNextIds(keys).values) { + val id = operator.entityFunctions.getEntityId(x) + val nextId = operator.entityFunctions.getEntityId(x) + entities.put(id, x) + idToPreviousId.put(nextId, id) + } + val modified = mutableSetOf() + val deleted = mutableSetOf() + var headModified = false + for (id in keys) { + val entity = entities[id] ?: continue + val isHead = operator.entityFunctions.getEntityIsHead(entity) + if (isHead) { + val nextId = operator.entityFunctions.getEntityNextId(entity) ?: continue + val nextEntity = entities[nextId] ?: continue + val nNextEntity = operator.entityFunctions.updateEntityIsHead(nextEntity, true) + entities.put(nextId, nNextEntity) + entities.remove(id) + modified.add(nextId) + modified.remove(id) + deleted.add(id) + headModified = true + operator.dataChangeFunctions?.onMarkedAsDeleted(id, entity) + continue + } + val previousId = idToPreviousId[id] ?: continue + val previousEntity = entities[previousId] ?: continue + val nextId = operator.entityFunctions.getEntityNextId(entity) + val nPreviousEntity = + operator.entityFunctions.updateEntityNextId(previousEntity, nextId) + entities.put(previousId, nPreviousEntity) + entities.remove(id) + modified.add(previousId) + modified.remove(id) + deleted.add(id) + operator.dataChangeFunctions?.onMarkedAsDeleted(id, entity) + } + val modifiedEntities = modified.mapNotNull { entities[it] }.toList() + return LazyLinkedListOperator.Changeset( + headModified = headModified, + modifiedKeys = modified.toList(), + modifiedEntities = modifiedEntities, + deletedKeys = deleted.toList(), + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListRemoveOperator.kt b/app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListRemoveOperator.kt deleted file mode 100644 index e19275eb..00000000 --- a/app/src/main/java/io/github/zyrouge/symphony/utils/lazy_linked_list/LazyLinkedListRemoveOperator.kt +++ /dev/null @@ -1,65 +0,0 @@ -package io.github.zyrouge.symphony.utils.lazy_linked_list - -class LazyLinkedListRemoveOperator( - val helper: LazyLinkedListOperatorHelper, - val keys: List, -) { - suspend fun operate(): LazyLinkedListOperatorHelper.Result { - if (keys.isEmpty()) { - return LazyLinkedListOperatorHelper.Result() - } - val entities = helper.persistenceFunctions.getEntitiesByIds(keys).toMutableMap() - val idToPreviousId = mutableMapOf() - for (x in entities.values) { - val id = helper.entityFunctions.getEntityId(x) - val nextId = helper.entityFunctions.getEntityId(x) - idToPreviousId[nextId] = id - } - for (x in helper.persistenceFunctions.getEntitiesByNextIds(keys).values) { - val id = helper.entityFunctions.getEntityId(x) - val nextId = helper.entityFunctions.getEntityId(x) - entities.put(id, x) - idToPreviousId.put(nextId, id) - } - val modified = mutableSetOf() - val deleted = mutableSetOf() - var headModified = false - for (id in keys) { - val entity = entities[id] ?: continue - val isHead = helper.entityFunctions.getEntityIsHead(entity) - if (isHead) { - val nextId = helper.entityFunctions.getEntityNextId(entity) ?: continue - val nextEntity = entities[nextId] ?: continue - val nNextEntity = helper.entityFunctions.updateEntityIsHead(nextEntity, true) - entities.put(nextId, nNextEntity) - entities.remove(id) - modified.add(nextId) - modified.remove(id) - deleted.add(id) - headModified = true - continue - } - val previousId = idToPreviousId[id] ?: continue - val previousEntity = entities[previousId] ?: continue - val nextId = helper.entityFunctions.getEntityNextId(entity) - val nPreviousEntity = helper.entityFunctions.updateEntityNextId(previousEntity, nextId) - entities.put(previousId, nPreviousEntity) - entities.remove(id) - modified.add(previousId) - modified.remove(id) - deleted.add(id) - } - if (modified.isNotEmpty()) { - val modifiedEntities = modified.mapNotNull { entities[it] }.toList() - helper.persistenceFunctions.updateEntities(modifiedEntities) - } - if (deleted.isNotEmpty()) { - helper.persistenceFunctions.deleteEntities(deleted.toList()) - } - return LazyLinkedListOperatorHelper.Result( - headModified = headModified, - modifiedKeys = modified.toList(), - deletedKeys = deleted.toList(), - ) - } -} \ No newline at end of file From bd73459330be46f1226ba15362c3033d3d0dd0ca Mon Sep 17 00:00:00 2001 From: Zyrouge Date: Sat, 27 Sep 2025 21:51:26 +0530 Subject: [PATCH 14/15] refactor: uh too many changes --- .../1.json | 769 ++++++++++++++---- .../zyrouge/symphony/services/Settings.kt | 11 - .../services/database/PersistentDatabase.kt | 3 +- .../services/database/store/AlbumStore.kt | 14 +- .../services/database/store/ArtistStore.kt | 17 +- .../services/database/store/ComposerStore.kt | 12 +- .../services/database/store/GenreStore.kt | 13 +- .../database/store/MediaTreeFolderStore.kt | 2 +- .../database/store/MediaTreeLyricFileStore.kt | 2 +- .../database/store/MediaTreeSongFileStore.kt | 2 +- .../store/PlaylistSongMappingStore.kt | 2 +- .../services/database/store/PlaylistStore.kt | 2 +- .../database/store/SongArtworkIndexStore.kt | 4 +- .../store/SongQueueSongMappingStore.kt | 17 + .../services/database/store/SongQueueStore.kt | 18 +- .../services/database/store/SongStore.kt | 2 +- .../symphony/services/groove/MediaExposer.kt | 9 +- .../services/groove/entities/Album.kt | 2 - .../groove/entities/AlbumComposerMapping.kt | 2 +- .../services/groove/entities/Artist.kt | 15 - .../groove/entities/ArtistSongMapping.kt | 2 +- .../services/groove/entities/Composer.kt | 2 - .../services/groove/entities/Genre.kt | 1 - .../groove/entities/MediaTreeFolder.kt | 1 - .../services/groove/entities/Playlist.kt | 3 +- .../groove/entities/PlaylistSongMapping.kt | 4 +- .../services/groove/entities/SongQueue.kt | 7 +- .../groove/entities/SongQueueSongMapping.kt | 3 +- .../groove/repositories/AlbumRepository.kt | 4 - .../groove/repositories/ArtistRepository.kt | 4 - .../groove/repositories/ComposerRepository.kt | 2 - .../groove/repositories/GenreRepository.kt | 3 - .../repositories/MediaTreeRepository.kt | 4 +- .../groove/repositories/PlaylistRepository.kt | 21 +- .../groove/repositories/SongRepository.kt | 32 +- .../zyrouge/symphony/services/radio/Radio.kt | 103 ++- .../symphony/services/radio/RadioPlayer.kt | 24 +- .../symphony/services/radio/RadioQueue.kt | 77 +- .../symphony/services/radio/RadioSession.kt | 2 +- .../symphony/services/radio/RadioShorty.kt | 89 -- .../ui/components/NewPlaylistDialog.kt | 19 +- .../ui/components/SongInformationDialog.kt | 67 +- .../symphony/ui/components/SongList.kt | 2 +- .../symphony/ui/components/SongTreeList.kt | 1 + .../symphony/ui/view/NowPlayingView.kt | 113 ++- .../ui/view/home/HomePlaylistsView.kt | 3 +- .../nowPlaying/NowPlayingBodyBottomBar.kt | 37 +- .../view/nowPlaying/NowPlayingBodyContent.kt | 7 +- .../ui/view/nowPlaying/NowPlayingBodyCover.kt | 3 +- .../nowPlaying/NowPlayingSleepTimerDialog.kt | 9 +- .../view/nowPlaying/NowPlayingSpeedDialog.kt | 19 +- .../ui/view/settings/SettingsGrooveView.kt | 1 + .../ui/view/settings/SettingsHomePageView.kt | 1 + .../view/settings/SettingsMiniPlayerView.kt | 1 + .../view/settings/SettingsNowPlayingView.kt | 1 + .../ui/view/settings/SettingsPlayerView.kt | 1 + .../ui/view/settings/SettingsUpdateView.kt | 1 + 57 files changed, 1073 insertions(+), 519 deletions(-) delete mode 100644 app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioShorty.kt diff --git a/app/room-schemas/io.github.zyrouge.symphony.services.database.PersistentDatabase/1.json b/app/room-schemas/io.github.zyrouge.symphony.services.database.PersistentDatabase/1.json index 56fea106..9e65cdac 100644 --- a/app/room-schemas/io.github.zyrouge.symphony.services.database.PersistentDatabase/1.json +++ b/app/room-schemas/io.github.zyrouge.symphony.services.database.PersistentDatabase/1.json @@ -2,56 +2,8 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "4818829a3e04d8013dca6a8e3a0317fd", + "identityHash": "5b83f644787184073d02f74ae9db9488", "entities": [ - { - "tableName": "albums", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `start_year` INTEGER, `end_year` INTEGER, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "startYear", - "columnName": "start_year", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "endYear", - "columnName": "end_year", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [ - { - "name": "index_albums_name", - "unique": false, - "columnNames": [ - "name" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_albums_name` ON `${TABLE_NAME}` (`name`)" - } - ], - "foreignKeys": [] - }, { "tableName": "album_artists_mapping", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`album_id` TEXT NOT NULL, `artist_id` TEXT NOT NULL, `is_album_artist` INTEGER NOT NULL, PRIMARY KEY(`album_id`, `artist_id`), FOREIGN KEY(`artist_id`) REFERENCES `albums`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artist_id`) REFERENCES `artists`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", @@ -136,6 +88,75 @@ } ] }, + { + "tableName": "album_composer_mapping", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`album_id` TEXT NOT NULL, `composer_id` TEXT NOT NULL, PRIMARY KEY(`album_id`, `composer_id`), FOREIGN KEY(`album_id`) REFERENCES `albums`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`composer_id`) REFERENCES `composers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "composerId", + "columnName": "composer_id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "album_id", + "composer_id" + ] + }, + "indices": [ + { + "name": "index_album_composer_mapping_album_id", + "unique": false, + "columnNames": [ + "album_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_album_composer_mapping_album_id` ON `${TABLE_NAME}` (`album_id`)" + }, + { + "name": "index_album_composer_mapping_composer_id", + "unique": false, + "columnNames": [ + "composer_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_album_composer_mapping_composer_id` ON `${TABLE_NAME}` (`composer_id`)" + } + ], + "foreignKeys": [ + { + "table": "albums", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "album_id" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "composers", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "composer_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, { "tableName": "album_songs_mapping", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`album_id` TEXT NOT NULL, `song_id` TEXT NOT NULL, PRIMARY KEY(`album_id`, `song_id`), FOREIGN KEY(`album_id`) REFERENCES `albums`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`song_id`) REFERENCES `songs`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", @@ -206,8 +227,8 @@ ] }, { - "tableName": "artists", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`id`))", + "tableName": "albums", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `start_year` INTEGER, `end_year` INTEGER, PRIMARY KEY(`id`))", "fields": [ { "fieldPath": "id", @@ -220,6 +241,18 @@ "columnName": "name", "affinity": "TEXT", "notNull": true + }, + { + "fieldPath": "startYear", + "columnName": "start_year", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "endYear", + "columnName": "end_year", + "affinity": "INTEGER", + "notNull": false } ], "primaryKey": { @@ -230,20 +263,20 @@ }, "indices": [ { - "name": "index_artists_name", + "name": "index_albums_name", "unique": false, "columnNames": [ "name" ], "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_artists_name` ON `${TABLE_NAME}` (`name`)" + "createSql": "CREATE INDEX IF NOT EXISTS `index_albums_name` ON `${TABLE_NAME}` (`name`)" } ], "foreignKeys": [] }, { "tableName": "artist_songs_mapping", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`artist_id` TEXT NOT NULL, `song_id` TEXT NOT NULL, PRIMARY KEY(`artist_id`, `song_id`), FOREIGN KEY(`artist_id`) REFERENCES `album_artists_mapping`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`song_id`) REFERENCES `songs`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`artist_id` TEXT NOT NULL, `song_id` TEXT NOT NULL, PRIMARY KEY(`artist_id`, `song_id`), FOREIGN KEY(`artist_id`) REFERENCES `artists`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`song_id`) REFERENCES `songs`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "artistId", @@ -287,7 +320,7 @@ ], "foreignKeys": [ { - "table": "album_artists_mapping", + "table": "artists", "onDelete": "CASCADE", "onUpdate": "NO ACTION", "columns": [ @@ -311,40 +344,97 @@ ] }, { - "tableName": "artwork_indices", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`song_id` TEXT NOT NULL, `file` TEXT, PRIMARY KEY(`song_id`), FOREIGN KEY(`song_id`) REFERENCES `songs`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "tableName": "artists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`id`))", "fields": [ { - "fieldPath": "songId", - "columnName": "song_id", + "fieldPath": "id", + "columnName": "id", "affinity": "TEXT", "notNull": true }, { - "fieldPath": "file", - "columnName": "file", + "fieldPath": "name", + "columnName": "name", "affinity": "TEXT", - "notNull": false + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_artists_name", + "unique": false, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_artists_name` ON `${TABLE_NAME}` (`name`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "composer_songs_mapping", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`composer_id` TEXT NOT NULL, `song_id` TEXT NOT NULL, PRIMARY KEY(`composer_id`, `song_id`), FOREIGN KEY(`composer_id`) REFERENCES `composers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`song_id`) REFERENCES `songs`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "composerId", + "columnName": "composer_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "songId", + "columnName": "song_id", + "affinity": "TEXT", + "notNull": true } ], "primaryKey": { "autoGenerate": false, "columnNames": [ + "composer_id", "song_id" ] }, "indices": [ { - "name": "index_artwork_indices_file", + "name": "index_composer_songs_mapping_composer_id", + "unique": false, + "columnNames": [ + "composer_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_composer_songs_mapping_composer_id` ON `${TABLE_NAME}` (`composer_id`)" + }, + { + "name": "index_composer_songs_mapping_song_id", "unique": false, "columnNames": [ - "file" + "song_id" ], "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_artwork_indices_file` ON `${TABLE_NAME}` (`file`)" + "createSql": "CREATE INDEX IF NOT EXISTS `index_composer_songs_mapping_song_id` ON `${TABLE_NAME}` (`song_id`)" } ], "foreignKeys": [ + { + "table": "composers", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "composer_id" + ], + "referencedColumns": [ + "id" + ] + }, { "table": "songs", "onDelete": "CASCADE", @@ -359,7 +449,7 @@ ] }, { - "tableName": "genres", + "tableName": "composers", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`id`))", "fields": [ { @@ -383,20 +473,20 @@ }, "indices": [ { - "name": "index_genres_name", + "name": "index_composers_name", "unique": false, "columnNames": [ "name" ], "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_genres_name` ON `${TABLE_NAME}` (`name`)" + "createSql": "CREATE INDEX IF NOT EXISTS `index_composers_name` ON `${TABLE_NAME}` (`name`)" } ], "foreignKeys": [] }, { "tableName": "genre_songs_mapping", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`genre_id` TEXT NOT NULL, `song_id` TEXT NOT NULL, PRIMARY KEY(`genre_id`, `song_id`), FOREIGN KEY(`genre_id`) REFERENCES `album_artists_mapping`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`song_id`) REFERENCES `songs`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`genre_id` TEXT NOT NULL, `song_id` TEXT NOT NULL, PRIMARY KEY(`genre_id`, `song_id`), FOREIGN KEY(`genre_id`) REFERENCES `genres`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`song_id`) REFERENCES `songs`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "genreId", @@ -440,7 +530,7 @@ ], "foreignKeys": [ { - "table": "album_artists_mapping", + "table": "genres", "onDelete": "CASCADE", "onUpdate": "NO ACTION", "columns": [ @@ -463,6 +553,42 @@ } ] }, + { + "tableName": "genres", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_genres_name", + "unique": false, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_genres_name` ON `${TABLE_NAME}` (`name`)" + } + ], + "foreignKeys": [] + }, { "tableName": "media_tree_folders", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `parent_id` TEXT, `name` TEXT NOT NULL, `is_head` INTEGER NOT NULL, `date_modified` INTEGER NOT NULL, `uri` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`parent_id`) REFERENCES `media_tree_folders`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", @@ -829,30 +955,42 @@ ] }, { - "tableName": "playlists", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `title` TEXT, `path` TEXT, PRIMARY KEY(`id`))", + "tableName": "playlist_songs_mapping", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`mapping_id` TEXT NOT NULL, `playlist_id` TEXT NOT NULL, `song_id` TEXT, `song_path` TEXT, `is_head` INTEGER NOT NULL, `next_id` TEXT, PRIMARY KEY(`mapping_id`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`song_id`) REFERENCES `songs`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`song_path`) REFERENCES `songs`(`path`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`next_id`) REFERENCES `playlist_songs_mapping`(`mapping_id`) ON UPDATE NO ACTION ON DELETE SET NULL )", "fields": [ { "fieldPath": "id", - "columnName": "id", + "columnName": "mapping_id", "affinity": "TEXT", "notNull": true }, { - "fieldPath": "title", - "columnName": "title", + "fieldPath": "playlistId", + "columnName": "playlist_id", "affinity": "TEXT", "notNull": true }, { - "fieldPath": "uri", - "columnName": "title", + "fieldPath": "songId", + "columnName": "song_id", "affinity": "TEXT", "notNull": false }, { - "fieldPath": "path", - "columnName": "path", + "fieldPath": "songPath", + "columnName": "song_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isHead", + "columnName": "is_head", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nextId", + "columnName": "next_id", "affinity": "TEXT", "notNull": false } @@ -860,25 +998,106 @@ "primaryKey": { "autoGenerate": false, "columnNames": [ - "id" + "mapping_id" ] }, "indices": [ { - "name": "index_playlists_title", + "name": "index_playlist_songs_mapping_playlist_id", "unique": false, "columnNames": [ - "title" + "playlist_id" ], "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_playlists_title` ON `${TABLE_NAME}` (`title`)" + "createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_songs_mapping_playlist_id` ON `${TABLE_NAME}` (`playlist_id`)" + }, + { + "name": "index_playlist_songs_mapping_song_id", + "unique": false, + "columnNames": [ + "song_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_songs_mapping_song_id` ON `${TABLE_NAME}` (`song_id`)" + }, + { + "name": "index_playlist_songs_mapping_song_path", + "unique": false, + "columnNames": [ + "song_path" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_songs_mapping_song_path` ON `${TABLE_NAME}` (`song_path`)" + }, + { + "name": "index_playlist_songs_mapping_is_head", + "unique": false, + "columnNames": [ + "is_head" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_songs_mapping_is_head` ON `${TABLE_NAME}` (`is_head`)" + }, + { + "name": "index_playlist_songs_mapping_next_id", + "unique": false, + "columnNames": [ + "next_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_songs_mapping_next_id` ON `${TABLE_NAME}` (`next_id`)" } ], - "foreignKeys": [] + "foreignKeys": [ + { + "table": "playlists", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "playlist_id" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "songs", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "song_id" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "songs", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "song_path" + ], + "referencedColumns": [ + "path" + ] + }, + { + "table": "playlist_songs_mapping", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "next_id" + ], + "referencedColumns": [ + "mapping_id" + ] + } + ] }, { - "tableName": "playlist_songs_mapping", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlist_id` TEXT NOT NULL, `song_id` TEXT, `song_path` TEXT, `is_head` INTEGER NOT NULL, `next_id` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`song_id`) REFERENCES `songs`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`song_path`) REFERENCES `songs`(`path`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`next_id`) REFERENCES `playlist_songs_mapping`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "tableName": "playlists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `internal_id` INTEGER, `title` TEXT NOT NULL, `uri` TEXT, `path` TEXT, PRIMARY KEY(`id`))", "fields": [ { "fieldPath": "id", @@ -887,25 +1106,168 @@ "notNull": true }, { - "fieldPath": "playlistId", - "columnName": "playlist_id", + "fieldPath": "internalId", + "columnName": "internal_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", "affinity": "TEXT", "notNull": true }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_playlists_internal_id", + "unique": true, + "columnNames": [ + "internal_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_playlists_internal_id` ON `${TABLE_NAME}` (`internal_id`)" + }, + { + "name": "index_playlists_title", + "unique": false, + "columnNames": [ + "title" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_playlists_title` ON `${TABLE_NAME}` (`title`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "song_artwork", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`song_id` TEXT NOT NULL, `file` TEXT, PRIMARY KEY(`song_id`), FOREIGN KEY(`song_id`) REFERENCES `songs`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ { "fieldPath": "songId", "columnName": "song_id", "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "file", + "columnName": "file", + "affinity": "TEXT", "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "song_id" + ] + }, + "indices": [ + { + "name": "index_song_artwork_song_id", + "unique": false, + "columnNames": [ + "song_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_song_artwork_song_id` ON `${TABLE_NAME}` (`song_id`)" + } + ], + "foreignKeys": [ + { + "table": "songs", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "song_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "song_lyrics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`song_file_id` TEXT NOT NULL, `lyrics` TEXT NOT NULL, PRIMARY KEY(`song_file_id`), FOREIGN KEY(`song_file_id`) REFERENCES `songs`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "songFileId", + "columnName": "song_file_id", + "affinity": "TEXT", + "notNull": true }, { - "fieldPath": "songPath", - "columnName": "song_path", + "fieldPath": "lyrics", + "columnName": "lyrics", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "song_file_id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "songs", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "song_file_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "song_queue_songs_mapping", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`mapping_id` TEXT NOT NULL, `queue_id` TEXT NOT NULL, `song_id` TEXT, `is_head` INTEGER NOT NULL, `next_id` TEXT, PRIMARY KEY(`mapping_id`), FOREIGN KEY(`queue_id`) REFERENCES `song_queue`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`song_id`) REFERENCES `songs`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`next_id`) REFERENCES `song_queue_songs_mapping`(`mapping_id`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "mapping_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "queueId", + "columnName": "queue_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "songId", + "columnName": "song_id", "affinity": "TEXT", "notNull": false }, { - "fieldPath": "isStart", + "fieldPath": "isHead", "columnName": "is_head", "affinity": "INTEGER", "notNull": true @@ -920,45 +1282,54 @@ "primaryKey": { "autoGenerate": false, "columnNames": [ - "id" + "mapping_id" ] }, "indices": [ { - "name": "index_playlist_songs_mapping_playlist_id", + "name": "index_song_queue_songs_mapping_queue_id", "unique": false, "columnNames": [ - "playlist_id" + "queue_id" ], "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_songs_mapping_playlist_id` ON `${TABLE_NAME}` (`playlist_id`)" + "createSql": "CREATE INDEX IF NOT EXISTS `index_song_queue_songs_mapping_queue_id` ON `${TABLE_NAME}` (`queue_id`)" }, { - "name": "index_playlist_songs_mapping_is_head", + "name": "index_song_queue_songs_mapping_song_id", + "unique": false, + "columnNames": [ + "song_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_song_queue_songs_mapping_song_id` ON `${TABLE_NAME}` (`song_id`)" + }, + { + "name": "index_song_queue_songs_mapping_is_head", "unique": false, "columnNames": [ "is_head" ], "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_songs_mapping_is_head` ON `${TABLE_NAME}` (`is_head`)" + "createSql": "CREATE INDEX IF NOT EXISTS `index_song_queue_songs_mapping_is_head` ON `${TABLE_NAME}` (`is_head`)" }, { - "name": "index_playlist_songs_mapping_next_id", + "name": "index_song_queue_songs_mapping_next_id", "unique": false, "columnNames": [ "next_id" ], "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_songs_mapping_next_id` ON `${TABLE_NAME}` (`next_id`)" + "createSql": "CREATE INDEX IF NOT EXISTS `index_song_queue_songs_mapping_next_id` ON `${TABLE_NAME}` (`next_id`)" } ], "foreignKeys": [ { - "table": "playlists", + "table": "song_queue", "onDelete": "CASCADE", "onUpdate": "NO ACTION", "columns": [ - "playlist_id" + "queue_id" ], "referencedColumns": [ "id" @@ -976,32 +1347,170 @@ ] }, { - "table": "songs", + "table": "song_queue_songs_mapping", "onDelete": "SET NULL", "onUpdate": "NO ACTION", "columns": [ - "song_path" + "next_id" ], "referencedColumns": [ - "path" + "mapping_id" ] + } + ] + }, + { + "tableName": "song_queue", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `original_id` TEXT, `internal_id` INTEGER, `playing_id` TEXT, `is_playing` INTEGER NOT NULL, `playing_timestamp` INTEGER NOT NULL, `playing_speed_int` INTEGER NOT NULL, `playing_pitch_int` INTEGER NOT NULL, `shuffled` INTEGER NOT NULL, `loop_mode` TEXT NOT NULL, `speed_int` INTEGER NOT NULL, `pitch_int` INTEGER NOT NULL, `pause_on_song_end` INTEGER NOT NULL, `sleep_timer_ends_at` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`original_id`) REFERENCES `song_queue`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`playing_id`) REFERENCES `song_queue_songs_mapping`(`mapping_id`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true }, { - "table": "playlist_songs_mapping", + "fieldPath": "originalId", + "columnName": "original_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "internalId", + "columnName": "internal_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "playingId", + "columnName": "playing_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isPlaying", + "columnName": "is_playing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playingTimestamp", + "columnName": "playing_timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playingSpeedInt", + "columnName": "playing_speed_int", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playingPitchInt", + "columnName": "playing_pitch_int", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shuffled", + "columnName": "shuffled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loopMode", + "columnName": "loop_mode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "speedInt", + "columnName": "speed_int", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pitchInt", + "columnName": "pitch_int", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pauseOnSongEnd", + "columnName": "pause_on_song_end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sleepTimerEndsAt", + "columnName": "sleep_timer_ends_at", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_song_queue_original_id", + "unique": false, + "columnNames": [ + "original_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_song_queue_original_id` ON `${TABLE_NAME}` (`original_id`)" + }, + { + "name": "index_song_queue_internal_id", + "unique": true, + "columnNames": [ + "internal_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_song_queue_internal_id` ON `${TABLE_NAME}` (`internal_id`)" + }, + { + "name": "index_song_queue_playing_id", + "unique": false, + "columnNames": [ + "playing_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_song_queue_playing_id` ON `${TABLE_NAME}` (`playing_id`)" + } + ], + "foreignKeys": [ + { + "table": "song_queue", "onDelete": "SET NULL", "onUpdate": "NO ACTION", "columns": [ - "next_id" + "original_id" ], "referencedColumns": [ "id" ] + }, + { + "table": "song_queue_songs_mapping", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "playing_id" + ], + "referencedColumns": [ + "mapping_id" + ] } ] }, { "tableName": "songs", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `track_number` INTEGER, `track_total` INTEGER, `disc_number` INTEGER, `disc_total` INTEGER, `date` TEXT, `year` INTEGER, `duration` INTEGER NOT NULL, `bitrate` INTEGER, `sampling_rate` INTEGER, `channels` INTEGER, `encoder` TEXT, `date_modified` INTEGER NOT NULL, `size` INTEGER NOT NULL, `uri` TEXT NOT NULL, `path` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `media_tree_song_files`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `track_number` INTEGER, `track_total` INTEGER, `disc_number` INTEGER, `disc_total` INTEGER, `date` TEXT, `year` INTEGER, `duration` INTEGER NOT NULL, `bitrate` INTEGER, `sampling_rate` INTEGER, `channels` INTEGER, `encoder` TEXT, `date_modified` INTEGER NOT NULL, `size` INTEGER NOT NULL, `filename` TEXT NOT NULL, `uri` TEXT NOT NULL, `path` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `media_tree_song_files`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "id", @@ -1093,6 +1602,12 @@ "affinity": "INTEGER", "notNull": true }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": true + }, { "fieldPath": "uri", "columnName": "uri", @@ -1145,50 +1660,12 @@ ] } ] - }, - { - "tableName": "song_lyrics", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`song_file_id` TEXT NOT NULL, `lyrics` TEXT NOT NULL, PRIMARY KEY(`song_file_id`), FOREIGN KEY(`song_file_id`) REFERENCES `songs`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "songFileId", - "columnName": "song_file_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "lyrics", - "columnName": "lyrics", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "song_file_id" - ] - }, - "indices": [], - "foreignKeys": [ - { - "table": "songs", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "song_file_id" - ], - "referencedColumns": [ - "id" - ] - } - ] } ], "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4818829a3e04d8013dca6a8e3a0317fd')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5b83f644787184073d02f74ae9db9488')" ] } } \ No newline at end of file diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/Settings.kt b/app/src/main/java/io/github/zyrouge/symphony/services/Settings.kt index 999f3332..55d241fd 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/Settings.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/Settings.kt @@ -10,7 +10,6 @@ import io.github.zyrouge.symphony.services.groove.repositories.GenreRepository import io.github.zyrouge.symphony.services.groove.repositories.MediaTreeRepository import io.github.zyrouge.symphony.services.groove.repositories.PlaylistRepository import io.github.zyrouge.symphony.services.groove.repositories.SongRepository -import io.github.zyrouge.symphony.services.radio.RadioQueue import io.github.zyrouge.symphony.ui.components.ResponsiveGridColumns import io.github.zyrouge.symphony.ui.theme.ThemeMode import io.github.zyrouge.symphony.ui.view.HomePage @@ -247,16 +246,6 @@ class Settings(private val symphony: Symphony) { ResponsiveGridColumns.DEFAULT_VERTICAL_COLUMNS, ) val lastDisabledTreePaths = StringSetEntry("last_disabled_tree_paths", emptySet()) - val previousSongQueue = object : Entry("previous_song_queue") { - override fun getValueInternal() = getSharedPreferences().getString(key, null)?.let { - RadioQueue.Serialized.parse(it) - } - - override fun setValueInternal(value: RadioQueue.Serialized?) = - getSharedPreferences().edit { - putString(key, value?.serialize()) - } - } val lastHomeTab = EnumEntry("home_last_page", enumEntries(), HomePage.Songs) val songsFilterPattern = NullableStringEntry("songs_filter_pattern") val minSongDuration = IntEntry("min_song_duration", 0) diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/PersistentDatabase.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/PersistentDatabase.kt index 14056bbb..0394b917 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/PersistentDatabase.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/PersistentDatabase.kt @@ -6,6 +6,7 @@ import androidx.room.RoomDatabase import androidx.room.TypeConverters import io.github.zyrouge.symphony.Symphony import io.github.zyrouge.symphony.services.database.store.AlbumArtistMappingStore +import io.github.zyrouge.symphony.services.database.store.AlbumComposerMappingStore import io.github.zyrouge.symphony.services.database.store.AlbumSongMappingStore import io.github.zyrouge.symphony.services.database.store.AlbumStore import io.github.zyrouge.symphony.services.database.store.ArtistSongMappingStore @@ -74,7 +75,7 @@ import io.github.zyrouge.symphony.utils.RoomConvertors @TypeConverters(RoomConvertors::class) abstract class PersistentDatabase : RoomDatabase() { abstract fun albumArtistMapping(): AlbumArtistMappingStore - abstract fun albumComposerMapping(): AlbumComposerMapping + abstract fun albumComposerMapping(): AlbumComposerMappingStore abstract fun albumSongMapping(): AlbumSongMappingStore abstract fun albums(): AlbumStore abstract fun artistSongMapping(): ArtistSongMappingStore diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumStore.kt index b2125b72..217df251 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumStore.kt @@ -16,7 +16,7 @@ import kotlinx.coroutines.flow.Flow @Dao abstract class AlbumStore { @Insert - abstract suspend fun insert(vararg entities: Album): List + abstract suspend fun insert(vararg entities: Album) @Update abstract suspend fun update(vararg entities: Album): Int @@ -53,6 +53,7 @@ abstract class AlbumStore { sortBy: AlbumRepository.SortBy, sortReverse: Boolean, artistId: String? = null, + songId: String? = null, ): Flow> { val aliasFirstArtist = "firstArtist" val embeddedArtistName = "firstArtistName" @@ -71,17 +72,22 @@ abstract class AlbumStore { "WHERE ${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID} = ${Album.COLUMN_ID} " + "ORDER BY ${AlbumArtistMapping.COLUMN_IS_ALBUM_ARTIST} DESC" val albumArtistMappingJoin = "" + - (if (artistId != null) "${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ARTIST_ID} = ? " else "") + + (if (artistId != null) "${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ARTIST_ID} = ? AND " else "") + "${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID} = ${Album.TABLE}.${Album.COLUMN_ID}" + val songMappingJoin = "" + + (if (songId != null) "${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_SONG_ID} = ? AND " else "") + + "${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_ALBUM_ID} = ${Album.TABLE}.${Album.COLUMN_ID}" val query = "SELECT ${Album.TABLE}.*, " + "COUNT(${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_SONG_ID}) as ${Album.AlongAttributes.EMBEDDED_TRACKS_COUNT}, " + "COUNT(${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ARTIST_ID}) as ${Album.AlongAttributes.EMBEDDED_ARTISTS_COUNT}, " + "$aliasFirstArtist.${Artist.COLUMN_NAME} as $embeddedArtistName" + "FROM ${Album.TABLE} " + - "LEFT JOIN ${AlbumSongMapping.TABLE} ON ${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_ALBUM_ID} = ${Album.TABLE}.${Album.COLUMN_ID} " + + "LEFT JOIN ${AlbumSongMapping.TABLE} ON $songMappingJoin " + "LEFT JOIN ${AlbumArtistMapping.TABLE} ON $albumArtistMappingJoin " + "LEFT JOIN ${Artist.TABLE} $aliasFirstArtist ON ${Artist.TABLE}.${Artist.COLUMN_ID} = ($artistQuery) " + "ORDER BY $orderBy $orderDirection" - return valuesAsFlow(SimpleSQLiteQuery(query)) + val args = mutableListOf() + songId?.let { args.add(it) } + return valuesAsFlow(SimpleSQLiteQuery(query, args.toTypedArray())) } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistStore.kt index f10f803a..7263b287 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistStore.kt @@ -18,7 +18,7 @@ import kotlinx.coroutines.flow.Flow @Dao abstract class ArtistStore { @Insert - abstract suspend fun insert(vararg entities: Artist): List + abstract suspend fun insert(vararg entities: Artist) @Update abstract suspend fun update(vararg entities: Artist): Int @@ -45,6 +45,7 @@ abstract class ArtistStore { fun valuesAsFlow( sortBy: ArtistRepository.SortBy, sortReverse: Boolean, + songId: String? = null, albumId: String? = null, onlyAlbumArtists: Boolean = false, ): Flow> { @@ -55,21 +56,23 @@ abstract class ArtistStore { ArtistRepository.SortBy.ALBUMS_COUNT -> Artist.AlongAttributes.EMBEDDED_ALBUMS_COUNT } val orderDirection = if (sortReverse) "DESC" else "ASC" + val songMappingJoin = "" + + (if (songId != null) "${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_SONG_ID} = ? AND " else "") + + "${ArtistSongMapping.TABLE}.${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_ARTIST_ID} = ${Artist.TABLE}.${Artist.COLUMN_ID}" val albumArtistMappingJoin = "" + - (if (albumId != null) "${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID} = ? " else "") + - (if (onlyAlbumArtists) "${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_IS_ALBUM_ARTIST} = 1 " else "") + + (if (albumId != null) "${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID} = ? AND " else "") + + (if (onlyAlbumArtists) "${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_IS_ALBUM_ARTIST} = 1 AND " else "") + "${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ARTIST_ID} = ${Artist.TABLE}.${Artist.COLUMN_ID}" val query = "SELECT ${Artist.TABLE}.*, " + "COUNT(${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_SONG_ID}) as ${Artist.AlongAttributes.EMBEDDED_TRACKS_COUNT}, " + "COUNT(${AlbumArtistMapping.TABLE}.${AlbumArtistMapping.COLUMN_ALBUM_ID}) as ${Artist.AlongAttributes.EMBEDDED_ALBUMS_COUNT} " + "FROM ${Artist.TABLE} " + - "LEFT JOIN ${ArtistSongMapping.TABLE} ON ${ArtistSongMapping.TABLE}.${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_ARTIST_ID} = ${Artist.TABLE}.${Artist.COLUMN_ID} " + + "LEFT JOIN ${ArtistSongMapping.TABLE} ON $songMappingJoin " + "LEFT JOIN ${AlbumArtistMapping.TABLE} ON $albumArtistMappingJoin " + "ORDER BY $orderBy $orderDirection" val args = mutableListOf() - if (albumId != null) { - args.add(albumId) - } + songId?.let { args.add(it) } + albumId?.let { args.add(it) } return valuesAsFlow(SimpleSQLiteQuery(query, args.toTypedArray())) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ComposerStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ComposerStore.kt index 4a777524..5f5fd71d 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ComposerStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ComposerStore.kt @@ -17,7 +17,7 @@ import kotlinx.coroutines.flow.Flow @Dao abstract class ComposerStore { @Insert - abstract suspend fun insert(vararg entities: Composer): List + abstract suspend fun insert(vararg entities: Composer) @Update abstract suspend fun update(vararg entities: Composer): Int @@ -28,6 +28,7 @@ abstract class ComposerStore { fun valuesAsFlow( sortBy: ComposerRepository.SortBy, sortReverse: Boolean, + songId: String? = null, ): Flow> { val orderBy = when (sortBy) { ComposerRepository.SortBy.CUSTOM -> "${Composer.TABLE}.${Composer.COLUMN_ID}" @@ -36,14 +37,19 @@ abstract class ComposerStore { ComposerRepository.SortBy.ALBUMS_COUNT -> Composer.AlongAttributes.EMBEDDED_ALBUMS_COUNT } val orderDirection = if (sortReverse) "DESC" else "ASC" + val songMappingJoin = "" + + (if (songId != null) " ${ComposerSongMapping.TABLE}.${ComposerSongMapping.COLUMN_SONG_ID} = ? AND " else "") + + "${ComposerSongMapping.TABLE}.${ComposerSongMapping.COLUMN_COMPOSER_ID} = ${Composer.TABLE}.${Composer.COLUMN_ID}" val query = "SELECT ${Composer.TABLE}.*, " + "COUNT(${ComposerSongMapping.TABLE}.${ComposerSongMapping.COLUMN_SONG_ID}) as ${Composer.AlongAttributes.EMBEDDED_TRACKS_COUNT}, " + "COUNT(${AlbumComposerMapping.TABLE}.${AlbumComposerMapping.COLUMN_ALBUM_ID}) as ${Composer.AlongAttributes.EMBEDDED_ALBUMS_COUNT} " + "FROM ${Composer.TABLE} " + - "LEFT JOIN ${ComposerSongMapping.TABLE} ON ${ComposerSongMapping.TABLE}.${ComposerSongMapping.COLUMN_COMPOSER_ID} = ${Composer.TABLE}.${Composer.COLUMN_ID} " + + "LEFT JOIN ${ComposerSongMapping.TABLE} ON $songMappingJoin " + "LEFT JOIN ${AlbumComposerMapping.TABLE} ON ${AlbumComposerMapping.TABLE}.${AlbumComposerMapping.COLUMN_COMPOSER_ID} = ${Composer.TABLE}.${Composer.COLUMN_ID}" + "ORDER BY $orderBy $orderDirection" - return valuesAsFlow(SimpleSQLiteQuery(query)) + val args = mutableListOf() + songId?.let { args.add(it) } + return valuesAsFlow(SimpleSQLiteQuery(query, args.toTypedArray())) } @RawQuery diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreStore.kt index 15f720ce..1fcb3593 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/GenreStore.kt @@ -15,7 +15,7 @@ import kotlinx.coroutines.flow.Flow @Dao abstract class GenreStore { @Insert - abstract suspend fun insert(vararg entities: Genre): List + abstract suspend fun insert(vararg entities: Genre) @RawQuery(observedEntities = [Genre::class, GenreSongMapping::class]) protected abstract fun findByIdAsFlow(query: SupportSQLiteQuery): Flow @@ -38,6 +38,7 @@ abstract class GenreStore { fun valuesAsFlow( sortBy: GenreRepository.SortBy, sortReverse: Boolean, + songId: String? = null, ): Flow> { val orderBy = when (sortBy) { GenreRepository.SortBy.CUSTOM -> "${Genre.TABLE}.${Genre.COLUMN_ID}" @@ -45,13 +46,17 @@ abstract class GenreStore { GenreRepository.SortBy.TRACKS_COUNT -> Genre.AlongAttributes.EMBEDDED_TRACKS_COUNT } val orderDirection = if (sortReverse) "DESC" else "ASC" + val songMappingJoin = "" + + (if (songId != null) "${GenreSongMapping.TABLE}.${GenreSongMapping.COLUMN_SONG_ID} = ? AND " else "") + + "${GenreSongMapping.TABLE}.${GenreSongMapping.COLUMN_GENRE_ID} = ${Genre.TABLE}.${Genre.COLUMN_ID}" val query = "SELECT ${Genre.TABLE}.*, " + "COUNT(${GenreSongMapping.TABLE}.${GenreSongMapping.COLUMN_SONG_ID}) as ${Genre.AlongAttributes.EMBEDDED_TRACKS_COUNT} " + "FROM ${Genre.TABLE} " + - "LEFT JOIN ${GenreSongMapping.TABLE} ON ${GenreSongMapping.TABLE}.${GenreSongMapping.COLUMN_GENRE_ID} = ${Genre.TABLE}.${Genre.COLUMN_ID} " + - "LEFT JOIN ${GenreSongMapping.TABLE} ON ${GenreSongMapping.TABLE}.${GenreSongMapping.COLUMN_GENRE_ID} = ${Genre.TABLE}.${Genre.COLUMN_ID} " + + "LEFT JOIN ${GenreSongMapping.TABLE} ON $songMappingJoin " + "ORDER BY $orderBy $orderDirection" - return valuesAsFlow(SimpleSQLiteQuery(query)) + val args = mutableListOf() + songId?.let { args.add(it) } + return valuesAsFlow(SimpleSQLiteQuery(query, args.toTypedArray())) } @RawQuery diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeFolderStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeFolderStore.kt index 71f58136..50215b3d 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeFolderStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeFolderStore.kt @@ -15,7 +15,7 @@ import kotlinx.coroutines.flow.Flow @Dao abstract class MediaTreeFolderStore { @Insert - abstract suspend fun insert(vararg entities: MediaTreeFolder): List + abstract suspend fun insert(vararg entities: MediaTreeFolder) @Update abstract suspend fun update(vararg entities: MediaTreeFolder): Int diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeLyricFileStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeLyricFileStore.kt index abd3209a..6f99fc74 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeLyricFileStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeLyricFileStore.kt @@ -11,7 +11,7 @@ import io.github.zyrouge.symphony.services.groove.entities.MediaTreeLyricFile @Dao abstract class MediaTreeLyricFileStore { @Insert - abstract suspend fun insert(vararg entities: MediaTreeLyricFile): List + abstract suspend fun insert(vararg entities: MediaTreeLyricFile) @Update abstract suspend fun update(vararg entities: MediaTreeLyricFile): Int diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeSongFileStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeSongFileStore.kt index 8d87c05e..4abfb7bb 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeSongFileStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/MediaTreeSongFileStore.kt @@ -12,7 +12,7 @@ import io.github.zyrouge.symphony.utils.builtin.sqlqph @Dao abstract class MediaTreeSongFileStore { @Insert - abstract suspend fun insert(vararg entities: MediaTreeSongFile): List + abstract suspend fun insert(vararg entities: MediaTreeSongFile) @Update abstract suspend fun update(vararg entities: MediaTreeSongFile): Int diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistSongMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistSongMappingStore.kt index d5c36df2..9c42c466 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistSongMappingStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistSongMappingStore.kt @@ -43,7 +43,7 @@ abstract class PlaylistSongMappingStore { return findTop4SongArtworksAsFlowRaw(SimpleSQLiteQuery(query, args)) } - @RawQuery + @RawQuery(observedEntities = [PlaylistSongMapping::class, Playlist::class]) protected abstract fun findSongIdsByPlaylistInternalIdAsFlowRaw(query: SimpleSQLiteQuery): Flow> @OptIn(ExperimentalCoroutinesApi::class) diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistStore.kt index c9aadb9f..1c69b363 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistStore.kt @@ -15,7 +15,7 @@ import kotlinx.coroutines.flow.Flow @Dao abstract class PlaylistStore { @Insert - abstract suspend fun insert(vararg entities: Playlist): List + abstract suspend fun insert(vararg entities: Playlist) @Update abstract suspend fun update(vararg entities: Playlist): Int diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongArtworkIndexStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongArtworkIndexStore.kt index 16892221..62b69268 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongArtworkIndexStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongArtworkIndexStore.kt @@ -12,9 +12,9 @@ import kotlinx.coroutines.flow.Flow @Dao abstract class SongArtworkIndexStore { @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract suspend fun upsert(vararg entities: SongArtworkIndex): List + abstract suspend fun upsert(vararg entities: SongArtworkIndex) - @RawQuery + @RawQuery(observedEntities = [SongArtworkIndex::class]) protected abstract fun findBySongIdAsFlow(query: SimpleSQLiteQuery): Flow fun findBySongIdAsFlow(songId: String): Flow { diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueSongMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueSongMappingStore.kt index f009c9cf..76a571ed 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueSongMappingStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueSongMappingStore.kt @@ -53,6 +53,19 @@ abstract class SongQueueSongMappingStore { return findById(SimpleSQLiteQuery(query, args)) } + @RawQuery(observedEntities = [SongQueueSongMapping::class, Song::class]) + protected abstract fun findByIdAsFlow(query: SupportSQLiteQuery): Flow + + fun findByIdAsFlow(queueId: String, id: String): Flow { + val query = "SELECT ${Song.TABLE}.*, " + + "${SongQueueSongMapping.TABLE}.* " + + "FROM ${SongQueueSongMapping.TABLE} " + + "WHERE ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_QUEUE_ID} = ? AND ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_ID} = ? " + + "LEFT JOIN ${Song.TABLE} ON ${Song.TABLE}.${Song.COLUMN_ID} = ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_SONG_ID} " + val args = arrayOf(queueId, id) + return findByIdAsFlow(SimpleSQLiteQuery(query, args)) + } + @RawQuery protected abstract fun findByNextId(query: SupportSQLiteQuery): Song.AlongSongQueueMapping? @@ -92,6 +105,7 @@ abstract class SongQueueSongMappingStore { return findHead(SimpleSQLiteQuery(query, args)) } + @RawQuery protected abstract fun entries(query: SupportSQLiteQuery): Map< @MapColumn(SongQueueSongMapping.COLUMN_ID) String, Song.AlongSongQueueMapping> @@ -107,6 +121,7 @@ abstract class SongQueueSongMappingStore { return entries(SimpleSQLiteQuery(query, args)) } + @RawQuery protected abstract fun entriesByIds(query: SupportSQLiteQuery): Map< @MapColumn(SongQueueSongMapping.COLUMN_ID) String, Song.AlongSongQueueMapping> @@ -124,6 +139,7 @@ abstract class SongQueueSongMappingStore { return entriesByIds(SimpleSQLiteQuery(query, args)) } + @RawQuery protected abstract fun entriesByNextIds(query: SupportSQLiteQuery): Map< @MapColumn(SongQueueSongMapping.COLUMN_NEXT_ID) String, Song.AlongSongQueueMapping> @@ -141,6 +157,7 @@ abstract class SongQueueSongMappingStore { return entriesByNextIds(SimpleSQLiteQuery(query, args)) } + @RawQuery protected abstract fun entriesBySongIds(query: SupportSQLiteQuery): Map< @MapColumn(SongQueueSongMapping.COLUMN_NEXT_ID) String, Song.AlongSongQueueMapping> diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueStore.kt index 256565ab..bceb8a06 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongQueueStore.kt @@ -13,7 +13,7 @@ import kotlinx.coroutines.flow.Flow @Dao abstract class SongQueueStore { @Insert - abstract suspend fun insert(vararg entities: SongQueue): List + abstract suspend fun insert(vararg entities: SongQueue) @Update abstract suspend fun update(vararg entities: SongQueue): Int @@ -30,7 +30,10 @@ abstract class SongQueueStore { protected abstract fun findById(query: SimpleSQLiteQuery): SongQueue.AlongAttributes? fun findById(id: String): SongQueue.AlongAttributes? { - val query = "SELECT * FROM ${SongQueue.TABLE} WHERE ${SongQueue.COLUMN_ID} = ?" + val query = "SELECT * FROM ${SongQueue.TABLE} " + + "FROM ${SongQueue.TABLE} " + + "LEFT JOIN ${SongQueueSongMapping.TABLE} ON ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_QUEUE_ID} = ${SongQueue.TABLE}.${SongQueue.COLUMN_ID} " + + "WHERE ${SongQueue.COLUMN_ID} = ?" val args = arrayOf(id) return findById(SimpleSQLiteQuery(query, args)) } @@ -42,7 +45,8 @@ abstract class SongQueueStore { val query = "SELECT ${SongQueue.TABLE}.*, " + "COUNT(${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_SONG_ID}) as ${SongQueue.AlongAttributes.EMBEDDED_TRACKS_COUNT} " + "FROM ${SongQueue.TABLE} " + - "LEFT JOIN ${SongQueueSongMapping.TABLE} ON ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_QUEUE_ID} = ${SongQueue.TABLE}.${SongQueue.COLUMN_ID}" + "LEFT JOIN ${SongQueueSongMapping.TABLE} ON ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_QUEUE_ID} = ${SongQueue.TABLE}.${SongQueue.COLUMN_ID} " + + "WHERE ${SongQueue.TABLE}.${SongQueue.COLUMN_INTERNAL_ID} = ?" val args = arrayOf(internalId) return findByInternalId(SimpleSQLiteQuery(query, args)) } @@ -50,11 +54,13 @@ abstract class SongQueueStore { @RawQuery(observedEntities = [SongQueue::class, SongQueueSongMapping::class]) protected abstract fun findByInternalIdAsFlow(query: SupportSQLiteQuery): Flow - fun findByInternalIdAsFlow(): Flow { + fun findByInternalIdAsFlow(internalId: Int): Flow { val query = "SELECT ${SongQueue.TABLE}.*, " + "COUNT(${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_SONG_ID}) as ${SongQueue.AlongAttributes.EMBEDDED_TRACKS_COUNT} " + "FROM ${SongQueue.TABLE} " + - "LEFT JOIN ${SongQueueSongMapping.TABLE} ON ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_QUEUE_ID} = ${SongQueue.TABLE}.${SongQueue.COLUMN_ID}" - return findByInternalIdAsFlow(SimpleSQLiteQuery(query)) + "LEFT JOIN ${SongQueueSongMapping.TABLE} ON ${SongQueueSongMapping.TABLE}.${SongQueueSongMapping.COLUMN_QUEUE_ID} = ${SongQueue.TABLE}.${SongQueue.COLUMN_ID} " + + "WHERE ${SongQueue.TABLE}.${SongQueue.COLUMN_INTERNAL_ID} = ?" + val args = arrayOf(internalId) + return findByInternalIdAsFlow(SimpleSQLiteQuery(query, args)) } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongStore.kt index e905df67..6c104602 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongStore.kt @@ -24,7 +24,7 @@ import kotlinx.coroutines.flow.Flow @Dao abstract class SongStore { @Insert - abstract suspend fun insert(vararg entities: Song): List + abstract suspend fun insert(vararg entities: Song) @Update abstract suspend fun update(vararg entities: Song): Int diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/MediaExposer.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/MediaExposer.kt index 38f52ed2..8e0630a8 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/MediaExposer.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/MediaExposer.kt @@ -21,11 +21,11 @@ import io.github.zyrouge.symphony.services.groove.entities.Song import io.github.zyrouge.symphony.services.groove.entities.SongArtworkIndex import io.github.zyrouge.symphony.services.groove.entities.SongLyric import io.github.zyrouge.symphony.utils.ActivityHelper -import io.github.zyrouge.symphony.utils.ConcurrentSet import io.github.zyrouge.symphony.utils.DocumentFileX import io.github.zyrouge.symphony.utils.ImagePreserver import io.github.zyrouge.symphony.utils.Logger import io.github.zyrouge.symphony.utils.SimplePath +import io.github.zyrouge.symphony.utils.builtin.ConcurrentSet import io.github.zyrouge.symphony.utils.builtin.concurrentSetOf import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async @@ -42,7 +42,7 @@ import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds class MediaExposer(private val symphony: Symphony) { - private val _isUpdating = MutableStateFlow(false) + private val _isUpdating = MutableStateFlow(false) val isUpdating get() = _isUpdating.asStateFlow() suspend fun fetch() { @@ -105,7 +105,7 @@ class MediaExposer(private val symphony: Symphony) { } } symphony.database.playlists.update(*playlistsToBeUpdated.toTypedArray()) - symphony.database.playlistSongMapping.deletePlaylistIds(playlistIdsToBeDeletedInMapping) + symphony.database.playlistSongMapping.deletePlaylistIds(*playlistIdsToBeDeletedInMapping.toTypedArray()) symphony.database.playlistSongMapping.insert(*playlistSongMappingToBeInserted.toTypedArray()) } catch (err: Exception) { Logger.error("MediaExposer", "playlist fetch failed", err) @@ -432,7 +432,7 @@ class MediaExposer(private val symphony: Symphony) { suspend fun cleanup() { try { - symphony.database.mediaTreeSongFiles.delete(songFileStaleIds) + symphony.database.mediaTreeSongFiles.delete(*songFileStaleIds.toTypedArray()) } catch (err: Exception) { Logger.warn("MediaExposer", "trimming song files failed", err) } @@ -517,5 +517,6 @@ class MediaExposer(private val symphony: Symphony) { companion object { const val MEDIA_TREE_ROOT_NAME = "root" + const val MIMETYPE_M3U = "audio/x-mpegurl" } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Album.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Album.kt index f1255f28..baef5ac3 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Album.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Album.kt @@ -26,9 +26,7 @@ data class Album( data class AlongAttributes( @Embedded val entity: Album, - @Embedded val tracksCount: Int, - @Embedded val artistsCount: Int, ) { companion object { diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/AlbumComposerMapping.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/AlbumComposerMapping.kt index 221bb5b4..08469a00 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/AlbumComposerMapping.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/AlbumComposerMapping.kt @@ -18,7 +18,7 @@ import androidx.room.Index onDelete = ForeignKey.CASCADE, ), ForeignKey( - entity = Artist::class, + entity = Composer::class, parentColumns = arrayOf(Composer.COLUMN_ID), childColumns = arrayOf(AlbumComposerMapping.COLUMN_COMPOSER_ID), onDelete = ForeignKey.CASCADE, diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Artist.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Artist.kt index 2d568a0f..2b93472e 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Artist.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Artist.kt @@ -6,7 +6,6 @@ import androidx.room.Embedded import androidx.room.Entity import androidx.room.Index import androidx.room.PrimaryKey -import io.github.zyrouge.symphony.Symphony @Immutable @Entity( @@ -23,9 +22,7 @@ data class Artist( data class AlongAttributes( @Embedded val entity: Artist, - @Embedded val tracksCount: Int, - @Embedded val albumsCount: Int, ) { companion object { @@ -34,18 +31,6 @@ data class Artist( } } - fun createArtworkImageRequest(symphony: Symphony) = - symphony.groove.artist.createArtworkImageRequest(name) - - fun getSongIds(symphony: Symphony) = symphony.groove.artist.getSongIds(name) - fun getSortedSongIds(symphony: Symphony) = symphony.groove.song.sort( - getSongIds(symphony), - symphony.settings.lastUsedSongsSortBy.value, - symphony.settings.lastUsedSongsSortReverse.value, - ) - - fun getAlbumIds(symphony: Symphony) = symphony.groove.artist.getAlbumIds(name) - companion object { const val TABLE = "artists" const val COLUMN_ID = "id" diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/ArtistSongMapping.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/ArtistSongMapping.kt index 4cc6fb3e..436d9682 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/ArtistSongMapping.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/ArtistSongMapping.kt @@ -12,7 +12,7 @@ import androidx.room.Index primaryKeys = [ArtistSongMapping.COLUMN_ARTIST_ID, ArtistSongMapping.COLUMN_SONG_ID], foreignKeys = [ ForeignKey( - entity = AlbumArtistMapping::class, + entity = Artist::class, parentColumns = arrayOf(Artist.COLUMN_ID), childColumns = arrayOf(ArtistSongMapping.COLUMN_ARTIST_ID), onDelete = ForeignKey.CASCADE, diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Composer.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Composer.kt index 81ffa539..c6bc9ff2 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Composer.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Composer.kt @@ -22,9 +22,7 @@ data class Composer( data class AlongAttributes( @Embedded val entity: Composer, - @Embedded val tracksCount: Int, - @Embedded val albumsCount: Int, ) { companion object { diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Genre.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Genre.kt index ca5072d6..c133c008 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Genre.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Genre.kt @@ -22,7 +22,6 @@ data class Genre( data class AlongAttributes( @Embedded val entity: Genre, - @Embedded val tracksCount: Int, ) { companion object { diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/MediaTreeFolder.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/MediaTreeFolder.kt index d002796b..22b4df5c 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/MediaTreeFolder.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/MediaTreeFolder.kt @@ -44,7 +44,6 @@ data class MediaTreeFolder( data class AlongAttributes( @Embedded val folder: MediaTreeFolder, - @Embedded val tracksCount: Int, ) { companion object { diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Playlist.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Playlist.kt index e5070c37..efffc201 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Playlist.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/Playlist.kt @@ -40,7 +40,6 @@ data class Playlist( data class AlongAttributes( @Embedded val entity: Playlist, - @Embedded val tracksCount: Int, ) { companion object { @@ -55,7 +54,7 @@ data class Playlist( const val COLUMN_ID = "id" const val COLUMN_INTERNAL_ID = "internal_id" const val COLUMN_TITLE = "title" - const val COLUMN_URI = "title" + const val COLUMN_URI = "uri" const val COLUMN_PATH = "path" fun parse(symphony: Symphony, id: String, uri: Uri): Parsed { diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/PlaylistSongMapping.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/PlaylistSongMapping.kt index 9d2f2f7d..5d2df0e8 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/PlaylistSongMapping.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/PlaylistSongMapping.kt @@ -38,6 +38,8 @@ import androidx.room.PrimaryKey ], indices = [ Index(PlaylistSongMapping.COLUMN_PLAYLIST_ID), + Index(PlaylistSongMapping.COLUMN_SONG_ID), + Index(PlaylistSongMapping.COLUMN_SONG_PATH), Index(PlaylistSongMapping.COLUMN_IS_HEAD), Index(PlaylistSongMapping.COLUMN_NEXT_ID), ], @@ -59,7 +61,7 @@ data class PlaylistSongMapping( ) { companion object { const val TABLE = "playlist_songs_mapping" - const val COLUMN_ID = "id" + const val COLUMN_ID = "mapping_id" const val COLUMN_PLAYLIST_ID = "playlist_id" const val COLUMN_SONG_ID = "song_id" const val COLUMN_SONG_PATH = "song_path" diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueue.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueue.kt index 5ee76959..6719a5d3 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueue.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueue.kt @@ -25,7 +25,11 @@ import androidx.room.PrimaryKey onDelete = ForeignKey.SET_NULL, ), ], - indices = [Index(SongQueue.COLUMN_INTERNAL_ID, unique = true)], + indices = [ + Index(SongQueue.COLUMN_ORIGINAL_ID), + Index(SongQueue.COLUMN_INTERNAL_ID, unique = true), + Index(SongQueue.COLUMN_PLAYING_ID), + ], ) data class SongQueue( @PrimaryKey @@ -71,7 +75,6 @@ data class SongQueue( data class AlongAttributes( @Embedded val entity: SongQueue, - @Embedded val tracksCount: Int, ) { companion object { diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueueSongMapping.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueueSongMapping.kt index 60e0922a..f499d09a 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueueSongMapping.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/SongQueueSongMapping.kt @@ -32,6 +32,7 @@ import androidx.room.PrimaryKey ], indices = [ Index(SongQueueSongMapping.COLUMN_QUEUE_ID), + Index(SongQueueSongMapping.COLUMN_SONG_ID), Index(SongQueueSongMapping.COLUMN_IS_HEAD), Index(SongQueueSongMapping.COLUMN_NEXT_ID), ], @@ -51,7 +52,7 @@ data class SongQueueSongMapping( ) { companion object { const val TABLE = "song_queue_songs_mapping" - const val COLUMN_ID = "id" + const val COLUMN_ID = "mapping_id" const val COLUMN_QUEUE_ID = "queue_id" const val COLUMN_SONG_ID = "song_id" const val COLUMN_IS_HEAD = "is_head" diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumRepository.kt index ed225956..1dd2e918 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumRepository.kt @@ -1,10 +1,6 @@ package io.github.zyrouge.symphony.services.groove.repositories import io.github.zyrouge.symphony.Symphony -import io.github.zyrouge.symphony.services.database.store.findByIdAsFlow -import io.github.zyrouge.symphony.services.database.store.findTop4SongArtworksAsFlow -import io.github.zyrouge.symphony.services.database.store.valuesAsFlow -import io.github.zyrouge.symphony.services.database.store.valuesMappedAsFlow import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.mapLatest diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/ArtistRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/ArtistRepository.kt index f62de8d0..0a111ffa 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/ArtistRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/ArtistRepository.kt @@ -1,10 +1,6 @@ package io.github.zyrouge.symphony.services.groove.repositories import io.github.zyrouge.symphony.Symphony -import io.github.zyrouge.symphony.services.database.store.findByIdAsFlow -import io.github.zyrouge.symphony.services.database.store.findTop4SongArtworksAsFlow -import io.github.zyrouge.symphony.services.database.store.valuesAsFlow -import io.github.zyrouge.symphony.services.database.store.valuesMappedAsFlow import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.mapLatest diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/ComposerRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/ComposerRepository.kt index 32bec624..b7da14cc 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/ComposerRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/ComposerRepository.kt @@ -1,8 +1,6 @@ package io.github.zyrouge.symphony.services.groove.repositories import io.github.zyrouge.symphony.Symphony -import io.github.zyrouge.symphony.services.database.store.findTop4SongArtworksAsFlow -import io.github.zyrouge.symphony.services.database.store.valuesAsFlow import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.mapLatest diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/GenreRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/GenreRepository.kt index 5dbb3778..73fc08e4 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/GenreRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/GenreRepository.kt @@ -1,9 +1,6 @@ package io.github.zyrouge.symphony.services.groove.repositories import io.github.zyrouge.symphony.Symphony -import io.github.zyrouge.symphony.services.database.store.findByIdAsFlow -import io.github.zyrouge.symphony.services.database.store.valuesAsFlow -import io.github.zyrouge.symphony.services.database.store.valuesMappedAsFlow class GenreRepository(private val symphony: Symphony) { enum class SortBy { diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/MediaTreeRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/MediaTreeRepository.kt index 470bf7c0..74207a3b 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/MediaTreeRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/MediaTreeRepository.kt @@ -1,8 +1,6 @@ package io.github.zyrouge.symphony.services.groove.repositories import io.github.zyrouge.symphony.Symphony -import io.github.zyrouge.symphony.services.database.store.findTop4SongArtworksAsFlow -import io.github.zyrouge.symphony.services.database.store.valuesAsFlow import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.mapLatest @@ -21,7 +19,7 @@ class MediaTreeRepository(private val symphony: Symphony) { } fun valuesAsFlow(sortBy: SortBy, sortReverse: Boolean) = - symphony.database.playlists.valuesAsFlow(sortBy, sortReverse) + symphony.database.mediaTreeFolders.valuesAsFlow(sortBy, sortReverse) companion object { private const val FAVORITE_PLAYLIST = "favorites" diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/PlaylistRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/PlaylistRepository.kt index a92085d7..c734cfa7 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/PlaylistRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/PlaylistRepository.kt @@ -4,7 +4,7 @@ import io.github.zyrouge.symphony.Symphony import io.github.zyrouge.symphony.services.groove.entities.Playlist import io.github.zyrouge.symphony.services.groove.entities.PlaylistSongMapping import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.launch @@ -15,16 +15,11 @@ class PlaylistRepository(private val symphony: Symphony) { TRACKS_COUNT, } - private val favoriteSongIdsFlow = symphony.database.playlistSongMapping - .findSongIdsByPlaylistInternalIdAsFlow(PLAYLIST_INTERNAL_ID_FAVORITES) + private lateinit var favoriteSongIdsFlow: Flow> private var favoriteSongIds = emptyList() init { - symphony.groove.coroutineScope.launch { - favoriteSongIdsFlow.collectLatest { - favoriteSongIds = it - } - } + observeFavoritesPlaylistChanges() } data class AddOptions( @@ -85,6 +80,16 @@ class PlaylistRepository(private val symphony: Symphony) { fun valuesAsFlow(sortBy: SortBy, sortReverse: Boolean) = symphony.database.playlists.valuesAsFlow(sortBy, sortReverse) + private fun observeFavoritesPlaylistChanges() { + favoriteSongIdsFlow = symphony.database.playlistSongMapping + .findSongIdsByPlaylistInternalIdAsFlow(PLAYLIST_INTERNAL_ID_FAVORITES) + symphony.groove.coroutineScope.launch { + favoriteSongIdsFlow.collect { + favoriteSongIds = it + } + } + } + companion object { const val PLAYLIST_INTERNAL_ID_FAVORITES = 1 } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/SongRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/SongRepository.kt index 0710459c..4336c238 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/SongRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/SongRepository.kt @@ -2,7 +2,6 @@ package io.github.zyrouge.symphony.services.groove.repositories import androidx.core.net.toUri import io.github.zyrouge.symphony.Symphony -import io.github.zyrouge.symphony.services.database.store.valuesAsFlow import io.github.zyrouge.symphony.services.groove.entities.SongArtworkIndex import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.mapLatest @@ -22,6 +21,37 @@ class SongRepository(private val symphony: Symphony) { TRACK_NUMBER, } + fun findArtistsOfIdAsFlow(id: String) = symphony.database.artists.valuesAsFlow( + ArtistRepository.SortBy.ARTIST_NAME, + false, + songId = id, + ) + + fun findAlbumArtistsOfIdAsFlow(id: String) = symphony.database.artists.valuesAsFlow( + ArtistRepository.SortBy.ARTIST_NAME, + false, + songId = id, + onlyAlbumArtists = true, + ) + + fun findComposersOfIdAsFlow(id: String) = symphony.database.composers.valuesAsFlow( + ComposerRepository.SortBy.COMPOSER_NAME, + false, + songId = id, + ) + + fun findAlbumsOfIdAsFlow(id: String) = symphony.database.albums.valuesAsFlow( + AlbumRepository.SortBy.ALBUM_NAME, + false, + songId = id, + ) + + fun findGenresOfIdAsFlow(id: String) = symphony.database.genres.valuesAsFlow( + GenreRepository.SortBy.GENRE, + false, + songId = id, + ) + @OptIn(ExperimentalCoroutinesApi::class) fun getArtworkUriAsFlow(id: String) = symphony.database.songArtworkIndices.findBySongIdAsFlow(id) diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/radio/Radio.kt b/app/src/main/java/io/github/zyrouge/symphony/services/radio/Radio.kt index 24f14f65..ad3a51bf 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/radio/Radio.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/radio/Radio.kt @@ -5,6 +5,8 @@ import io.github.zyrouge.symphony.Symphony import io.github.zyrouge.symphony.SymphonyHooks import io.github.zyrouge.symphony.services.groove.entities.Song import io.github.zyrouge.symphony.services.groove.entities.SongQueue +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import java.time.Instant import java.util.Date @@ -14,12 +16,15 @@ class Radio(private val symphony: Symphony) : SymphonyHooks { data class SleepTimer( val endsAt: Long, val quitOnEnd: Boolean, - val timer: Timer, - ) + ) { + data class Internal(val sleepTimer: SleepTimer, val timer: Timer) + } private val queue = RadioQueue(symphony) private val player = RadioPlayer(symphony) - private var sleepTimer: SleepTimer? = null + private var sleepTimerInterval: SleepTimer.Internal? = null + val sleepTimer = MutableStateFlow(null) + val mediaSessionId get() = player.mediaSessionId suspend fun play(): Boolean { if (player.hasMedia()) { @@ -49,7 +54,6 @@ class Radio(private val symphony: Symphony) : SymphonyHooks { } suspend fun play(songMappingId: String): Boolean { - player.stop() val songQueue = queue.getCurrentSongQueue() ?: return false val songQueueId = songQueue.entity.id val song = symphony.database.songQueueSongMapping.findById(songQueueId, songMappingId) @@ -61,14 +65,21 @@ class Radio(private val symphony: Symphony) : SymphonyHooks { private suspend fun play( songMappingId: String, songUri: Uri, - seek: Long = 0, - speed: Float = 1f, - pitch: Float = 1f, + seek: Long = RadioPlayer.DEFAULT_SEEK, + speed: Float = RadioPlayer.DEFAULT_SPEED, + pitch: Float = RadioPlayer.DEFAULT_PITCH, ) { - if (player.getMedia() == songUri) { + if (player.getMedia()?.uri == songUri) { player.play() return } + player.stop() + val nMedia = RadioPlayer.PlayableMedia(songMappingId, songUri) + player.setMedia(nMedia) + player.seek(seek) + player.setSpeed(speed) + player.setPitch(pitch) + player.play() queue.updateCurrentSongQueue { it.entity.copy( playingId = songMappingId, @@ -78,13 +89,6 @@ class Radio(private val symphony: Symphony) : SymphonyHooks { playingPitchInt = (pitch * SongQueue.PITCH_MULTIPLIER).toInt(), ) } - val media = RadioPlayer.PlayableMedia(songMappingId, songUri) - player.setMedia(media) - player.seek(seek) - player.setSpeed(speed) - player.setPitch(pitch) - player.play() - return } suspend fun pause(): Boolean { @@ -129,17 +133,13 @@ class Radio(private val symphony: Symphony) : SymphonyHooks { return true } - suspend fun setSleepTimer(endsAt: Long?): Boolean { + suspend fun setSleepTimer(sleepTimer: SleepTimer): Boolean { val success = queue.updateCurrentSongQueue { it.entity.copy(sleepTimerEndsAt = null) } if (!success) { return false } - cancelSleepTimer() - if (endsAt == null) { - return true - } val quitOnEnd = false val timerTask = kotlin.concurrent.timerTask { val shouldQuit = quitOnEnd @@ -152,15 +152,17 @@ class Radio(private val symphony: Symphony) : SymphonyHooks { } } val timer = Timer() - timer.schedule(timerTask, Date.from(Instant.ofEpochMilli(endsAt))) + timer.schedule(timerTask, Date.from(Instant.ofEpochMilli(sleepTimer.endsAt))) cancelSleepTimer() - sleepTimer = SleepTimer(endsAt = endsAt, quitOnEnd = quitOnEnd, timer = timer) + sleepTimerInterval = SleepTimer.Internal(sleepTimer = sleepTimer, timer = timer) + this.sleepTimer.update { sleepTimerInterval?.sleepTimer } return true } - private fun cancelSleepTimer() { - sleepTimer?.timer?.cancel() - sleepTimer = null + fun cancelSleepTimer() { + sleepTimerInterval?.timer?.cancel() + sleepTimerInterval = null + sleepTimer.update { sleepTimerInterval?.sleepTimer } } suspend fun setPauseOnCurrentSongEnd(value: Boolean) = queue.updateCurrentSongQueue { @@ -207,12 +209,41 @@ class Radio(private val symphony: Symphony) : SymphonyHooks { suspend fun remove(songMappingId: String) = queue.remove(songMappingId) - internal fun onCurrentSongEnded() { + internal fun onQueueCurrentPlayingSongChanged(song: Song.AlongSongQueueMapping?) { + symphony.groove.coroutineScope.launch { + onQueueCurrentPlayingSongChangedNeedsSuspend(song) + } + } + internal suspend fun onQueueCurrentPlayingSongChangedNeedsSuspend(song: Song.AlongSongQueueMapping?) { +// if (song == null) { +// player.stop() +// return +// } +// val media = player.getMedia() +// if (media?.uri == song.entity.uri) { +// return +// } +// play(song.mapping.id, song.entity.uri) } - internal fun onNextSongChange() { + internal fun onQueueNextPlayingSongChanged(song: Song.AlongSongQueueMapping?) { + symphony.groove.coroutineScope.launch { + onQueueNextPlayingSongChangedNeedsSuspend(song) + } + } + internal suspend fun onQueueNextPlayingSongChangedNeedsSuspend(song: Song.AlongSongQueueMapping?) { + if (song == null) { + player.setNextMedia(null) + return + } + val nextMedia = player.getNextMedia() + if (nextMedia?.uri == song.entity.uri) { + return + } + val nNextMedia = RadioPlayer.PlayableMedia(song.mapping.id, song.entity.uri) + player.setNextMedia(nNextMedia) } internal enum class SongEndedReason { @@ -227,15 +258,15 @@ class Radio(private val symphony: Symphony) : SymphonyHooks { } private suspend fun onPlayerSongEndedNeedsSuspend(source: SongEndedReason) { - val songQueue = queue.getCurrentSongQueue() ?: return - val queueId = songQueue.entity.id - val media = player.getMedia() ?: return - val song = symphony.database.songQueueSongMapping.findById(queueId, media.id) ?: return - val nextSongMappingId = song.mapping.nextId ?: return - val nextSong = - symphony.database.songQueueSongMapping.findById(queueId, nextSongMappingId) ?: return - val nextMedia = RadioPlayer.PlayableMedia(nextSongMappingId, nextSong.entity.uri) - player.setNextMedia(nextMedia) +// val songQueue = queue.getCurrentSongQueue() ?: return +// val queueId = songQueue.entity.id +// val media = player.getMedia() ?: return +// val song = symphony.database.songQueueSongMapping.findById(queueId, media.id) ?: return +// val nextSongMappingId = song.mapping.nextId ?: return +// val nextSong = +// symphony.database.songQueueSongMapping.findById(queueId, nextSongMappingId) ?: return +// val nextMedia = RadioPlayer.PlayableMedia(nextSongMappingId, nextSong.entity.uri) +// player.setNextMedia(nextMedia) } internal fun onPlayerIsPlayingChanged(isPlaying: Boolean) { diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioPlayer.kt b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioPlayer.kt index 4e5bec43..8aebe76f 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioPlayer.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioPlayer.kt @@ -77,7 +77,7 @@ class RadioPlayer(val symphony: Symphony) { private val interceptingMediaPlayer = InterceptedMediaPlayer(this, mediaPlayerUnsafe) private val mediaSessionCallback = MediaSessionCallback(this) - private val mediaSession = MediaSession + val mediaSession = MediaSession .Builder(symphony.applicationContext, interceptingMediaPlayer) .setCallback(mediaSessionCallback) .build() @@ -115,12 +115,25 @@ class RadioPlayer(val symphony: Symphony) { } suspend fun setMedia(media: PlayableMedia) = withMediaPlayer { - it.setMediaItems(listOf(media.toMediaItem())) + val mediaItem = media.toMediaItem() + when (it.mediaItemCount) { + 0 -> it.setMediaItems(listOf(mediaItem)) + else -> it.replaceMediaItem(0, mediaItem) + } } - suspend fun setNextMedia(media: PlayableMedia) = withMediaPlayer { - it.replaceMediaItem(1, media.toMediaItem()) - it.play() + suspend fun setNextMedia(media: PlayableMedia?) = withMediaPlayer { + if (media == null) { + it.removeMediaItem(1) + return@withMediaPlayer + } + val mediaItem = media.toMediaItem() + when (it.mediaItemCount) { + // this shouldn't happen + 0 -> it.setMediaItems(listOf(mediaItem, mediaItem)) + 1 -> it.addMediaItem(mediaItem) + else -> it.replaceMediaItem(1, mediaItem) + } } suspend fun play() = withMediaPlayer { @@ -202,6 +215,7 @@ class RadioPlayer(val symphony: Symphony) { const val MIN_VOLUME = 0f const val MAX_VOLUME = 1f const val DUCK_VOLUME = 0.2f + const val DEFAULT_SEEK = 0L const val DEFAULT_SPEED = 1f const val DEFAULT_PITCH = 1f } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioQueue.kt b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioQueue.kt index e2a98747..23c71be8 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioQueue.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioQueue.kt @@ -8,6 +8,12 @@ import io.github.zyrouge.symphony.services.groove.entities.Song import io.github.zyrouge.symphony.services.groove.entities.SongQueue import io.github.zyrouge.symphony.services.groove.entities.SongQueueSongMapping import io.github.zyrouge.symphony.utils.lazy_linked_list.LazyLinkedListOperator +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.distinctUntilChangedBy +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.launch class RadioQueue(private val symphony: Symphony) { private class SongQueueSongMappingOperatorEntityFunctions : @@ -64,30 +70,8 @@ class RadioQueue(private val symphony: Symphony) { private interface SongQueueSongMappingOperatorDataChangeFunctions : LazyLinkedListOperator.DataChangeFunctions -// val queueFlow = symphony.database.songQueue.findFirstAsFlow() -// val songQueue = AtomicReference(null) - -// @OptIn(ExperimentalCoroutinesApi::class) -// val queueSongsFlow = queueFlow.transformLatest { -// if (it == null) { -// emit(emptyList()) -// return@transformLatest -// } -// emitAll(symphony.database.songQueueSongMapping.valuesAsFlow(it.entity.id)) -// } -// val queueSongs = AtomicReference>(emptyList()) - init { -// symphony.groove.coroutineScope.launch { -// queueFlow.collect { -// queue.set(it) -// } -// } -// symphony.groove.coroutineScope.launch { -// queueSongsFlow.collect { -// queueSongs.set(it) -// } -// } + observeSongQueueChanges() } sealed class AddPosition { @@ -298,7 +282,7 @@ class RadioQueue(private val symphony: Symphony) { return true } - fun getCurrentSongQueue() = + internal fun getCurrentSongQueue() = symphony.database.songQueue.findByInternalId(SONG_QUEUE_INTERNAL_ID_DEFAULT) private suspend fun createOrGetCurrentSongQueue(): SongQueue { @@ -337,6 +321,51 @@ class RadioQueue(private val symphony: Symphony) { dataChangeFunctions, ) + private fun observeSongQueueChanges() { + symphony.groove.coroutineScope.launch { + observeSongQueueChangesNeedsSuspend() + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + private suspend fun observeSongQueueChangesNeedsSuspend() { + val songQueueFlow = + symphony.database.songQueue.findByInternalIdAsFlow(SONG_QUEUE_INTERNAL_ID_DEFAULT) + .distinctUntilChanged() + val currentSongFlow = songQueueFlow + .distinctUntilChangedBy { it?.entity?.playingId } + .flatMapLatest { + val queueId = it?.entity?.id + val playingId = it?.entity?.playingId + // ugly code since keeps throwing elvis incorrect warning + if (queueId != null && playingId != null) { + symphony.database.songQueueSongMapping.findByIdAsFlow(queueId, playingId) + } else { + emptyFlow() + } + } + val nextSongFlow = currentSongFlow + .distinctUntilChangedBy { it?.mapping?.nextId } + .flatMapLatest { + val queueId = it?.mapping?.queueId + val nextSongMappingId = it?.mapping?.nextId + if (queueId != null && nextSongMappingId != null) { + symphony.database.songQueueSongMapping.findByIdAsFlow( + queueId, + nextSongMappingId + ) + } else { + emptyFlow() + } + } + currentSongFlow.collect { + symphony.radio.onQueueCurrentPlayingSongChanged(it) + } + nextSongFlow.collect { + symphony.radio.onQueueNextPlayingSongChanged(it) + } + } + companion object { const val SONG_QUEUE_INTERNAL_ID_DEFAULT = 1 } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioSession.kt b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioSession.kt index 86cf4a61..c155a610 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioSession.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioSession.kt @@ -176,7 +176,7 @@ class RadioSession(val symphony: Symphony) { input: Unit, ) = Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL).apply { putExtra(AudioEffect.EXTRA_PACKAGE_NAME, symphony.applicationContext.packageName) - putExtra(AudioEffect.EXTRA_AUDIO_SESSION, symphony.radio.player.mediaSessionId) + putExtra(AudioEffect.EXTRA_AUDIO_SESSION, symphony.radio.mediaSessionId) putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioShorty.kt b/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioShorty.kt deleted file mode 100644 index 9cae963d..00000000 --- a/app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioShorty.kt +++ /dev/null @@ -1,89 +0,0 @@ -package io.github.zyrouge.symphony.services.radio - -import io.github.zyrouge.symphony.Symphony -import io.github.zyrouge.symphony.services.groove.entities.Song -import kotlin.random.Random - -class RadioShorty(private val symphony: Symphony) { - fun playPause() { - if (!symphony.radio.hasPlayer) { - return - } - when { - symphony.radio.isPlaying -> symphony.radio.pause() - else -> symphony.radio.resume() - } - } - - fun seekFromCurrent(offsetSecs: Int) { - if (!symphony.radio.hasPlayer) { - return - } - symphony.radio.currentPlaybackPosition?.run { - val to = (played + (offsetSecs * 1000)).coerceIn(0..total) - symphony.radio.seek(to) - } - } - - fun previous() = when { - !symphony.radio.hasPlayer -> false - symphony.radio.currentPlaybackPosition!!.played <= 3000 && symphony.radio.canJumpToPrevious() -> { - symphony.radio.jumpToPrevious() - true - } - - else -> { - symphony.radio.seek(0) - false - } - } - - fun skip() = when { - !symphony.radio.hasPlayer -> false - symphony.radio.canJumpToNext() -> { - symphony.radio.jumpToNext() - true - } - - else -> { - symphony.radio.play(Radio.PlayOptions(index = 0, autostart = false)) - false - } - } - - fun playQueue( - songIds: List, - options: Radio.PlayOptions = Radio.PlayOptions(), - shuffle: Boolean = false, - ) { - symphony.radio.stop(ended = false) - if (songIds.isEmpty()) { - return - } - symphony.radio.queue.add( - songIds, - options = options.run { - copy(index = if (shuffle) Random.nextInt(songIds.size) else options.index) - } - ) - symphony.radio.queue.setShuffleMode(shuffle) - } - - fun playQueue( - songId: String, - options: Radio.PlayOptions = Radio.PlayOptions(), - shuffle: Boolean = false, - ) = playQueue(listOf(songId), options = options, shuffle = shuffle) - - fun playQueue( - songs: List, - options: Radio.PlayOptions = Radio.PlayOptions(), - shuffle: Boolean = false, - ) = playQueue(songs.map { it.id }, options = options, shuffle = shuffle) - - fun playQueue( - song: Song, - options: Radio.PlayOptions = Radio.PlayOptions(), - shuffle: Boolean = false, - ) = playQueue(listOf(song.id), options = options, shuffle = shuffle) -} diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/NewPlaylistDialog.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/NewPlaylistDialog.kt index d7abebde..a198009c 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/NewPlaylistDialog.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/NewPlaylistDialog.kt @@ -101,14 +101,15 @@ fun NewPlaylistDialog( ) if (showSongsPicker) { - PlaylistManageSongsDialog( - context, - selectedSongIds = songIdsImmutable, - onDone = { - showSongsPicker = false - songIds.clear() - songIds.addAll(it) - } - ) +// TODO +// PlaylistManageSongsDialog( +// context, +// selectedSongIds = songIdsImmutable, +// onDone = { +// showSongsPicker = false +// songIds.clear() +// songIds.addAll(it) +// } +// ) } } \ No newline at end of file diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongInformationDialog.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongInformationDialog.kt index 20147d57..f0715f71 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongInformationDialog.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongInformationDialog.kt @@ -6,12 +6,13 @@ import androidx.compose.foundation.layout.FlowRow import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.text.style.TextDecoration -import io.github.zyrouge.symphony.services.groove.Song +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.zyrouge.symphony.services.groove.entities.Song import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.ui.view.AlbumArtistViewRoute import io.github.zyrouge.symphony.ui.view.AlbumViewRoute import io.github.zyrouge.symphony.ui.view.ArtistViewRoute import io.github.zyrouge.symphony.ui.view.GenreViewRoute @@ -23,6 +24,17 @@ import kotlin.math.round @Composable fun SongInformationDialog(context: ViewContext, song: Song, onDismissRequest: () -> Unit) { + val artists by context.symphony.groove.song.findArtistsOfIdAsFlow(song.id) + .collectAsStateWithLifecycle(emptyList()) + val albumArtists by context.symphony.groove.song.findAlbumArtistsOfIdAsFlow(song.id) + .collectAsStateWithLifecycle(emptyList()) + val composers by context.symphony.groove.song.findComposersOfIdAsFlow(song.id) + .collectAsStateWithLifecycle(emptyList()) + val albums by context.symphony.groove.song.findAlbumsOfIdAsFlow(song.id) + .collectAsStateWithLifecycle(emptyList()) + val genres by context.symphony.groove.song.findGenresOfIdAsFlow(song.id) + .collectAsStateWithLifecycle(emptyList()) + InformationDialog( context, content = { @@ -32,42 +44,47 @@ fun SongInformationDialog(context: ViewContext, song: Song, onDismissRequest: () InformationKeyValue(context.symphony.t.TrackName) { LongPressCopyableText(context, song.title) } - if (song.artists.isNotEmpty()) { + if (artists.isNotEmpty()) { + val artistNamesMap = artists.associate { it.entity.id to it.entity.name } InformationKeyValue(context.symphony.t.Artist) { - LongPressCopyableAndTappableText(context, song.artists) { + LongPressCopyableAndTappableText(context, artistNamesMap) { onDismissRequest() context.navController.navigate(ArtistViewRoute(it)) } } } - if (song.albumArtists.isNotEmpty()) { + if (albumArtists.isNotEmpty()) { + val albumArtistNamesMap = albumArtists.associate { it.entity.id to it.entity.name } InformationKeyValue(context.symphony.t.AlbumArtist) { - LongPressCopyableAndTappableText(context, song.albumArtists) { + LongPressCopyableAndTappableText(context, albumArtistNamesMap) { onDismissRequest() - context.navController.navigate(AlbumArtistViewRoute(it)) + context.navController.navigate(ArtistViewRoute(it)) } } } - if (song.composers.isNotEmpty()) { + if (composers.isNotEmpty()) { + val composerNamesMap = albumArtists.associate { it.entity.id to it.entity.name } InformationKeyValue(context.symphony.t.Composer) { // TODO composers page maybe? - LongPressCopyableAndTappableText(context, song.composers) { + LongPressCopyableAndTappableText(context, composerNamesMap) { onDismissRequest() - context.navController.navigate(ArtistViewRoute(it)) +// context.navController.navigate(ArtistViewRoute(it)) } } } - context.symphony.groove.album.getIdFromSong(song)?.let { albumId -> - InformationKeyValue(context.symphony.t.Album) { - LongPressCopyableAndTappableText(context, setOf(song.album!!)) { + if (albums.isNotEmpty()) { + val albumNamesMap = albums.associate { it.entity.id to it.entity.name } + InformationKeyValue(context.symphony.t.Composer) { + LongPressCopyableAndTappableText(context, albumNamesMap) { onDismissRequest() - context.navController.navigate(AlbumViewRoute(albumId)) + context.navController.navigate(AlbumViewRoute(it)) } } } - if (song.genres.isNotEmpty()) { + if (genres.isNotEmpty()) { + val genreNamesMap = albums.associate { it.entity.id to it.entity.name } InformationKeyValue(context.symphony.t.Genre) { - LongPressCopyableAndTappableText(context, song.genres) { + LongPressCopyableAndTappableText(context, genreNamesMap) { onDismissRequest() context.navController.navigate(GenreViewRoute(it)) } @@ -151,27 +168,25 @@ fun SongInformationDialog(context: ViewContext, song: Song, onDismissRequest: () @OptIn(ExperimentalLayoutApi::class) @Composable -private fun LongPressCopyableAndTappableText( +private fun LongPressCopyableAndTappableText( context: ViewContext, - values: Set, - onTap: (String) -> Unit, + values: Map, + onTap: (T) -> Unit, ) { - val textStyle = LocalTextStyle.current.copy( - textDecoration = TextDecoration.Underline, - ) + val textStyle = LocalTextStyle.current.copy(textDecoration = TextDecoration.Underline) FlowRow { - values.forEachIndexed { i, it -> + values.entries.forEachIndexed { i, x -> Text( - it, + x.value, style = textStyle, modifier = Modifier.pointerInput(Unit) { detectTapGestures( onLongPress = { _ -> - ActivityHelper.copyToClipboardAndNotify(context.symphony, it) + ActivityHelper.copyToClipboardAndNotify(context.symphony, x.value) }, onTap = { _ -> - onTap(it) + onTap(x.key) }, ) }, diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongList.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongList.kt index 5bbc0ae5..84d38373 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongList.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongList.kt @@ -64,7 +64,7 @@ fun SongList( Text(context.symphony.t.XSongs(songs.size.toString())) }, onShufflePlay = { - context.symphony.radio.shorty.playQueue(songs, shuffle = true) + context.symphony.radio.playQueue(songs, shuffle = true) } ) }, diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongTreeList.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongTreeList.kt index 0bf9c63d..030a281a 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongTreeList.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongTreeList.kt @@ -53,6 +53,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.compose.AsyncImage import io.github.zyrouge.symphony.services.groove.repositories.SongRepository import io.github.zyrouge.symphony.services.radio.Radio diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/NowPlayingView.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/NowPlayingView.kt index 304b117e..4331d384 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/NowPlayingView.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/NowPlayingView.kt @@ -2,15 +2,11 @@ package io.github.zyrouge.symphony.ui.view import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.zyrouge.symphony.services.groove.entities.Song -import io.github.zyrouge.symphony.services.radio.RadioQueue +import io.github.zyrouge.symphony.services.groove.entities.SongQueue import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.ui.view.nowPlaying.NowPlayingNothingPlaying import io.github.zyrouge.symphony.ui.view.nowPlaying.NowPlayingBody +import io.github.zyrouge.symphony.ui.view.nowPlaying.NowPlayingNothingPlaying import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.serialization.Serializable @@ -20,7 +16,7 @@ data class NowPlayingData( val isPlaying: Boolean, val currentSongIndex: Int, val queueSize: Int, - val currentLoopMode: RadioQueue.LoopMode, + val currentLoopMode: SongQueue.LoopMode, val currentShuffleMode: Boolean, val currentSpeed: Float, val currentPitch: Float, @@ -73,55 +69,56 @@ fun NowPlayingObserver( context: ViewContext, content: @Composable (NowPlayingData?) -> Unit, ) { - val queue by context.symphony.radio.observatory.queue.collectAsStateWithLifecycle() - val song by remember { - derivedStateOf { - queue.firstOrNull { it. } - } - } - val isViable by remember(song) { - derivedStateOf { song != null } - } - - val isPlaying by context.symphony.radio.observatory.isPlaying.collectAsStateWithLifecycle() - val currentLoopMode by context.symphony.radio.observatory.loopMode.collectAsStateWithLifecycle() - val currentShuffleMode by context.symphony.radio.observatory.shuffleMode.collectAsStateWithLifecycle() - val currentSpeed by context.symphony.radio.observatory.speed.collectAsStateWithLifecycle() - val currentPitch by context.symphony.radio.observatory.pitch.collectAsStateWithLifecycle() - val persistedSpeed by context.symphony.radio.observatory.persistedSpeed.collectAsStateWithLifecycle() - val persistedPitch by context.symphony.radio.observatory.persistedPitch.collectAsStateWithLifecycle() - val sleepTimer by context.symphony.radio.observatory.sleepTimer.collectAsStateWithLifecycle() - val pauseOnCurrentSongEnd by context.symphony.radio.observatory.pauseOnCurrentSongEnd.collectAsStateWithLifecycle() - val showSongAdditionalInfo by context.symphony.settings.nowPlayingAdditionalInfo.flow.collectAsStateWithLifecycle() - val enableSeekControls by context.symphony.settings.nowPlayingSeekControls.flow.collectAsStateWithLifecycle() - val seekBackDuration by context.symphony.settings.seekBackDuration.flow.collectAsStateWithLifecycle() - val seekForwardDuration by context.symphony.settings.seekForwardDuration.flow.collectAsStateWithLifecycle() - val controlsLayout by context.symphony.settings.nowPlayingControlsLayout.flow.collectAsStateWithLifecycle() - val lyricsLayout by context.symphony.settings.nowPlayingLyricsLayout.flow.collectAsStateWithLifecycle() - - val data = when { - isViable -> NowPlayingData( - song = song!!, - isPlaying = isPlaying, - currentSongIndex = queueIndex, - queueSize = queue.size, - currentLoopMode = currentLoopMode, - currentShuffleMode = currentShuffleMode, - currentSpeed = currentSpeed, - currentPitch = currentPitch, - persistedSpeed = persistedSpeed, - persistedPitch = persistedPitch, - hasSleepTimer = sleepTimer != null, - pauseOnCurrentSongEnd = pauseOnCurrentSongEnd, - showSongAdditionalInfo = showSongAdditionalInfo, - enableSeekControls = enableSeekControls, - seekBackDuration = seekBackDuration, - seekForwardDuration = seekForwardDuration, - controlsLayout = controlsLayout, - lyricsLayout = lyricsLayout, - ) - - else -> null - } - content(data) +// TODO +// val queue by context.symphony.radio.observatory.queue.collectAsStateWithLifecycle() +// val song by remember { +// derivedStateOf { +// queue.firstOrNull { it. } +// } +// } +// val isViable by remember(song) { +// derivedStateOf { song != null } +// } +// +// val isPlaying by context.symphony.radio.observatory.isPlaying.collectAsStateWithLifecycle() +// val currentLoopMode by context.symphony.radio.observatory.loopMode.collectAsStateWithLifecycle() +// val currentShuffleMode by context.symphony.radio.observatory.shuffleMode.collectAsStateWithLifecycle() +// val currentSpeed by context.symphony.radio.observatory.speed.collectAsStateWithLifecycle() +// val currentPitch by context.symphony.radio.observatory.pitch.collectAsStateWithLifecycle() +// val persistedSpeed by context.symphony.radio.observatory.persistedSpeed.collectAsStateWithLifecycle() +// val persistedPitch by context.symphony.radio.observatory.persistedPitch.collectAsStateWithLifecycle() +// val sleepTimer by context.symphony.radio.observatory.sleepTimer.collectAsStateWithLifecycle() +// val pauseOnCurrentSongEnd by context.symphony.radio.observatory.pauseOnCurrentSongEnd.collectAsStateWithLifecycle() +// val showSongAdditionalInfo by context.symphony.settings.nowPlayingAdditionalInfo.flow.collectAsStateWithLifecycle() +// val enableSeekControls by context.symphony.settings.nowPlayingSeekControls.flow.collectAsStateWithLifecycle() +// val seekBackDuration by context.symphony.settings.seekBackDuration.flow.collectAsStateWithLifecycle() +// val seekForwardDuration by context.symphony.settings.seekForwardDuration.flow.collectAsStateWithLifecycle() +// val controlsLayout by context.symphony.settings.nowPlayingControlsLayout.flow.collectAsStateWithLifecycle() +// val lyricsLayout by context.symphony.settings.nowPlayingLyricsLayout.flow.collectAsStateWithLifecycle() +// +// val data = when { +// isViable -> NowPlayingData( +// song = song!!, +// isPlaying = isPlaying, +// currentSongIndex = queueIndex, +// queueSize = queue.size, +// currentLoopMode = currentLoopMode, +// currentShuffleMode = currentShuffleMode, +// currentSpeed = currentSpeed, +// currentPitch = currentPitch, +// persistedSpeed = persistedSpeed, +// persistedPitch = persistedPitch, +// hasSleepTimer = sleepTimer != null, +// pauseOnCurrentSongEnd = pauseOnCurrentSongEnd, +// showSongAdditionalInfo = showSongAdditionalInfo, +// enableSeekControls = enableSeekControls, +// seekBackDuration = seekBackDuration, +// seekForwardDuration = seekForwardDuration, +// controlsLayout = controlsLayout, +// lyricsLayout = lyricsLayout, +// ) +// +// else -> null +// } +// content(data) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/HomePlaylistsView.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/HomePlaylistsView.kt index 3c4a1218..22178019 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/HomePlaylistsView.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/HomePlaylistsView.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.zyrouge.symphony.services.groove.MediaExposer import io.github.zyrouge.symphony.services.groove.entities.Playlist import io.github.zyrouge.symphony.services.groove.repositories.PlaylistRepository import io.github.zyrouge.symphony.ui.components.LoaderScaffold @@ -84,7 +85,7 @@ fun HomePlaylistsView(context: ViewContext) { showPlaylistCreator = true }, showPlaylistPicker = { - openPlaylistLauncher.launch(arrayOf(Playlist.MIMETYPE_M3U)) + openPlaylistLauncher.launch(arrayOf(MediaExposer.MIMETYPE_M3U)) }, ) Spacer(modifier = Modifier.height(4.dp)) diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingBodyBottomBar.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingBodyBottomBar.kt index 047e008d..4f1f9c83 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingBodyBottomBar.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingBodyBottomBar.kt @@ -1,7 +1,11 @@ package io.github.zyrouge.symphony.ui.view.nowPlaying +import android.content.Context +import android.content.Intent +import android.media.audiofx.AudioEffect import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContract import androidx.activity.result.launch import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column @@ -45,6 +49,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.zyrouge.symphony.services.radio.RadioQueue import io.github.zyrouge.symphony.ui.helpers.ViewContext import io.github.zyrouge.symphony.ui.view.LyricsViewRoute @@ -63,12 +68,34 @@ fun NowPlayingBodyBottomBar( data: NowPlayingData, states: NowPlayingStates, ) { + val viewContext = context val coroutineScope = rememberCoroutineScope() - val equalizerActivity = rememberLauncherForActivityResult( - context.symphony.radio.session.createEqualizerActivityContract() - ) {} + val equalizerContract = remember { + object : ActivityResultContract() { + override fun createIntent( + context: Context, + input: Unit, + ) = Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL).apply { + putExtra( + AudioEffect.EXTRA_PACKAGE_NAME, + viewContext.symphony.applicationContext.packageName + ) + putExtra( + AudioEffect.EXTRA_AUDIO_SESSION, + viewContext.symphony.radio.mediaSessionId + ) + putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC) + } - val sleepTimer by context.symphony.radio.observatory.sleepTimer.collectAsStateWithLifecycle() + override fun parseResult( + resultCode: Int, + intent: Intent?, + ) { + } + } + } + val equalizerActivity = rememberLauncherForActivityResult(equalizerContract) {} + val sleepTimer by context.symphony.radio.sleepTimer.collectAsStateWithLifecycle() var showSleepTimerDialog by remember { mutableStateOf(false) } var showSpeedDialog by remember { mutableStateOf(false) } var showPitchDialog by remember { mutableStateOf(false) } @@ -135,7 +162,7 @@ fun NowPlayingBodyBottomBar( } IconButton( onClick = { - context.symphony.radio.queue.toggleLoopMode() + context.symphony.radio.toggleLoopMode() } ) { Icon( diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingBodyContent.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingBodyContent.kt index 774b7765..863d0fd3 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingBodyContent.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingBodyContent.kt @@ -36,7 +36,6 @@ import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf @@ -62,10 +61,8 @@ import io.github.zyrouge.symphony.utils.DurationHelper @OptIn(ExperimentalLayoutApi::class) @Composable fun NowPlayingBodyContent(context: ViewContext, data: NowPlayingData) { - val favoriteSongIds by context.symphony.groove.playlist.favorites.collectAsStateWithLifecycle() - val isFavorite by remember(data) { - derivedStateOf { favoriteSongIds.contains(data.song.id) } - } + val isFavorite by context.symphony.groove.playlist.isFavoriteSongAsFlow(data.song.id) + .collectAsStateWithLifecycle(false) data.run { Column { diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingBodyCover.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingBodyCover.kt index 1deb0a66..fc2baa57 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingBodyCover.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingBodyCover.kt @@ -24,8 +24,9 @@ import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.min +import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.compose.AsyncImage -import io.github.zyrouge.symphony.services.groove.Song +import io.github.zyrouge.symphony.services.groove.entities.Song import io.github.zyrouge.symphony.ui.components.KeepScreenAwake import io.github.zyrouge.symphony.ui.components.LyricsText import io.github.zyrouge.symphony.ui.components.TimedContentTextStyle diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingSleepTimerDialog.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingSleepTimerDialog.kt index 7a947de1..fa619869 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingSleepTimerDialog.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingSleepTimerDialog.kt @@ -108,7 +108,7 @@ fun NowPlayingSleepTimerDialog( Spacer(modifier = Modifier.weight(1f)) TextButton( onClick = { - context.symphony.radio.clearSleepTimer() + context.symphony.radio.cancelSleepTimer() onDismissRequest() } ) { @@ -244,10 +244,9 @@ fun NowPlayingSleepTimerSetDialog( TextButton( enabled = isValidDuration, onClick = { - context.symphony.radio.setSleepTimer( - duration = inputDuration, - quitOnEnd = quitOnEnd, - ) + val endsAt = System.currentTimeMillis() + inputDuration + val sleepTimer = Radio.SleepTimer(endsAt = endsAt, quitOnEnd = quitOnEnd) + context.symphony.radio.setSleepTimer(sleepTimer) onDismissRequest() } ) { diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingSpeedDialog.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingSpeedDialog.kt index acff6e29..89610791 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingSpeedDialog.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NowPlayingSpeedDialog.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.unit.dp import io.github.zyrouge.symphony.ui.components.ScaffoldDialog import io.github.zyrouge.symphony.ui.components.Slider import io.github.zyrouge.symphony.ui.helpers.ViewContext +import kotlinx.coroutines.launch import kotlin.math.roundToInt @Composable @@ -58,8 +59,10 @@ fun NowPlayingSpeedDialog( .padding(top = 16.dp), ) { allowedSpeeds.forEach { speed -> - val onClick = { - context.symphony.radio.setSpeed(speed, persistent) + val onClick: () -> Unit = { + context.symphony.groove.coroutineScope.launch { + context.symphony.radio.setSpeed(speed, persistent) + } } val shape = RoundedCornerShape(4.dp) @@ -81,7 +84,9 @@ fun NowPlayingSpeedDialog( value = currentSpeed, onChange = { value -> val speed = (value * 10).roundToInt().toFloat() / 10 - context.symphony.radio.setSpeed(speed, persistent) + context.symphony.groove.coroutineScope.launch { + context.symphony.radio.setSpeed(speed, persistent) + } }, range = allowedSpeedRange, label = { value -> @@ -99,7 +104,9 @@ fun NowPlayingSpeedDialog( checked = persistent, onCheckedChange = { persistent = !persistent - context.symphony.radio.setSpeed(currentSpeed, persistent) + context.symphony.groove.coroutineScope.launch { + context.symphony.radio.setSpeed(currentSpeed, persistent) + } } ) Spacer(modifier = Modifier.width(8.dp)) @@ -110,7 +117,9 @@ fun NowPlayingSpeedDialog( actions = { TextButton( onClick = { - context.symphony.radio.setSpeed(1f, persistent) + context.symphony.groove.coroutineScope.launch { + context.symphony.radio.setSpeed(1f, persistent) + } onDismissRequest() } ) { diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsGrooveView.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsGrooveView.kt index 8ca621a1..e2a157d1 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsGrooveView.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsGrooveView.kt @@ -41,6 +41,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.graphics.Color +import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.zyrouge.symphony.Symphony import io.github.zyrouge.symphony.services.groove.Groove import io.github.zyrouge.symphony.ui.components.AdaptiveSnackbar diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsHomePageView.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsHomePageView.kt index 09b1234a..343a7f28 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsHomePageView.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsHomePageView.kt @@ -23,6 +23,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.zyrouge.symphony.ui.components.IconButtonPlaceholder import io.github.zyrouge.symphony.ui.components.TopAppBarMinimalTitle import io.github.zyrouge.symphony.ui.components.settings.ConsiderContributingTile diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsMiniPlayerView.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsMiniPlayerView.kt index cca792ac..bbc8ef80 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsMiniPlayerView.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsMiniPlayerView.kt @@ -23,6 +23,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.zyrouge.symphony.ui.components.IconButtonPlaceholder import io.github.zyrouge.symphony.ui.components.TopAppBarMinimalTitle import io.github.zyrouge.symphony.ui.components.settings.ConsiderContributingTile diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsNowPlayingView.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsNowPlayingView.kt index 29bfa5d9..530bde87 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsNowPlayingView.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsNowPlayingView.kt @@ -25,6 +25,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.zyrouge.symphony.ui.components.IconButtonPlaceholder import io.github.zyrouge.symphony.ui.components.TopAppBarMinimalTitle import io.github.zyrouge.symphony.ui.components.settings.ConsiderContributingTile diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsPlayerView.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsPlayerView.kt index 8bb838d8..5b7c1a43 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsPlayerView.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsPlayerView.kt @@ -27,6 +27,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.zyrouge.symphony.ui.components.IconButtonPlaceholder import io.github.zyrouge.symphony.ui.components.TopAppBarMinimalTitle import io.github.zyrouge.symphony.ui.components.settings.ConsiderContributingTile diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsUpdateView.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsUpdateView.kt index b9daf853..fe2dd605 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsUpdateView.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsUpdateView.kt @@ -21,6 +21,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.zyrouge.symphony.ui.components.IconButtonPlaceholder import io.github.zyrouge.symphony.ui.components.TopAppBarMinimalTitle import io.github.zyrouge.symphony.ui.components.settings.ConsiderContributingTile From 4d8413384b722cb35f835630ba25cfc88e7f22e9 Mon Sep 17 00:00:00 2001 From: Zyrouge Date: Tue, 14 Oct 2025 17:10:39 +0530 Subject: [PATCH 15/15] refactor: ui changes --- .phrasey/schema.toml | 3 + .../1.json | 27 +-- .../database/store/AlbumSongMappingStore.kt | 12 ++ .../database/store/ArtistSongMappingStore.kt | 12 ++ .../store/PlaylistSongMappingStore.kt | 119 ++++++++++- .../services/database/store/SongStore.kt | 11 +- .../symphony/services/groove/MediaExposer.kt | 4 +- .../groove/entities/PlaylistSongMapping.kt | 14 +- .../groove/repositories/AlbumRepository.kt | 15 ++ .../groove/repositories/ArtistRepository.kt | 8 + .../groove/repositories/PlaylistRepository.kt | 185 +++++++++++++++--- .../groove/repositories/SongRepository.kt | 4 +- .../zyrouge/symphony/services/radio/Radio.kt | 14 ++ .../ui/components/AddToPlaylistDialog.kt | 66 +++++-- .../symphony/ui/components/AlbumArtistTile.kt | 130 ------------ .../symphony/ui/components/AlbumGrid.kt | 1 + .../symphony/ui/components/AlbumTile.kt | 75 +++++-- .../symphony/ui/components/ArtistTile.kt | 61 ++++-- .../ui/components/GenericGrooveArtworkGrid.kt | 65 ++++++ .../ui/components/GenericGrooveBanner.kt | 4 +- .../ui/components/GenericGrooveCard.kt | 4 +- .../ui/components/GenericSongListDropdown.kt | 2 +- .../ui/components/IntroductoryDialog.kt | 1 + .../ui/components/NewPlaylistDialog.kt | 41 ++-- .../symphony/ui/components/PlaylistTile.kt | 89 +++++---- .../ui/components/RenamePlaylistDialog.kt | 9 +- .../symphony/ui/components/SongCard.kt | 2 +- .../ui/components/SquareGrooveTile.kt | 5 +- .../symphony/ui/helpers/UserInterface.kt | 17 +- .../zyrouge/symphony/ui/view/PlaylistView.kt | 1 + .../zyrouge/symphony/ui/view/QueueView.kt | 2 +- .../zyrouge/symphony/ui/view/home/ForYou.kt | 139 +++++-------- .../ui/view/home/HomePlaylistsView.kt | 11 +- .../ui/view/settings/SettingsGrooveView.kt | 2 +- i18n/en.toml | 3 +- 35 files changed, 746 insertions(+), 412 deletions(-) delete mode 100644 app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumArtistTile.kt create mode 100644 app/src/main/java/io/github/zyrouge/symphony/ui/components/GenericGrooveArtworkGrid.kt diff --git a/.phrasey/schema.toml b/.phrasey/schema.toml index 16b5613a..0a4fe75d 100644 --- a/.phrasey/schema.toml +++ b/.phrasey/schema.toml @@ -762,3 +762,6 @@ name = "KeepScreenAwakeOnLyrics" [[keys]] name = "MinSongDurationFilter" + +[[keys]] +name = "ArtistCount" diff --git a/app/room-schemas/io.github.zyrouge.symphony.services.database.PersistentDatabase/1.json b/app/room-schemas/io.github.zyrouge.symphony.services.database.PersistentDatabase/1.json index 9e65cdac..2640b6e3 100644 --- a/app/room-schemas/io.github.zyrouge.symphony.services.database.PersistentDatabase/1.json +++ b/app/room-schemas/io.github.zyrouge.symphony.services.database.PersistentDatabase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "5b83f644787184073d02f74ae9db9488", + "identityHash": "e4c7854a47ab573f1abed106e30810e5", "entities": [ { "tableName": "album_artists_mapping", @@ -956,7 +956,7 @@ }, { "tableName": "playlist_songs_mapping", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`mapping_id` TEXT NOT NULL, `playlist_id` TEXT NOT NULL, `song_id` TEXT, `song_path` TEXT, `is_head` INTEGER NOT NULL, `next_id` TEXT, PRIMARY KEY(`mapping_id`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`song_id`) REFERENCES `songs`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`song_path`) REFERENCES `songs`(`path`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`next_id`) REFERENCES `playlist_songs_mapping`(`mapping_id`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`mapping_id` TEXT NOT NULL, `playlist_id` TEXT NOT NULL, `song_id` TEXT, `raw_song_path` TEXT, `is_head` INTEGER NOT NULL, `next_id` TEXT, PRIMARY KEY(`mapping_id`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`song_id`) REFERENCES `songs`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`next_id`) REFERENCES `playlist_songs_mapping`(`mapping_id`) ON UPDATE NO ACTION ON DELETE SET NULL )", "fields": [ { "fieldPath": "id", @@ -977,8 +977,8 @@ "notNull": false }, { - "fieldPath": "songPath", - "columnName": "song_path", + "fieldPath": "rawSongPath", + "columnName": "raw_song_path", "affinity": "TEXT", "notNull": false }, @@ -1021,13 +1021,13 @@ "createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_songs_mapping_song_id` ON `${TABLE_NAME}` (`song_id`)" }, { - "name": "index_playlist_songs_mapping_song_path", + "name": "index_playlist_songs_mapping_raw_song_path", "unique": false, "columnNames": [ - "song_path" + "raw_song_path" ], "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_songs_mapping_song_path` ON `${TABLE_NAME}` (`song_path`)" + "createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_songs_mapping_raw_song_path` ON `${TABLE_NAME}` (`raw_song_path`)" }, { "name": "index_playlist_songs_mapping_is_head", @@ -1071,17 +1071,6 @@ "id" ] }, - { - "table": "songs", - "onDelete": "SET NULL", - "onUpdate": "NO ACTION", - "columns": [ - "song_path" - ], - "referencedColumns": [ - "path" - ] - }, { "table": "playlist_songs_mapping", "onDelete": "SET NULL", @@ -1665,7 +1654,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5b83f644787184073d02f74ae9db9488')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e4c7854a47ab573f1abed106e30810e5')" ] } } \ No newline at end of file diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumSongMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumSongMappingStore.kt index a16de5c8..0547938b 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumSongMappingStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/AlbumSongMappingStore.kt @@ -29,6 +29,18 @@ abstract class AlbumSongMappingStore { return findTop4SongArtworksAsFlow(SimpleSQLiteQuery(query, args)) } + fun valuesMapped( + songStore: SongStore, + id: String, + sortBy: SongRepository.SortBy, + sortReverse: Boolean, + ) = songStore.values( + sortBy, + sortReverse, + additionalClauseBeforeJoins = "JOIN ${AlbumSongMapping.TABLE}.${AlbumSongMapping.COLUMN_ALBUM_ID} = ? ", + additionalArgsBeforeJoins = arrayOf(id), + ) + fun valuesMappedAsFlow( songStore: SongStore, id: String, diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistSongMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistSongMappingStore.kt index 38857224..20aa053c 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistSongMappingStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtistSongMappingStore.kt @@ -29,6 +29,18 @@ abstract class ArtistSongMappingStore { return findTop4SongArtworksAsFlow(SimpleSQLiteQuery(query, args)) } + fun valuesMapped( + songStore: SongStore, + id: String, + sortBy: SongRepository.SortBy, + sortReverse: Boolean, + ) = songStore.values( + sortBy, + sortReverse, + additionalClauseBeforeJoins = "JOIN ${ArtistSongMapping.TABLE}.${ArtistSongMapping.COLUMN_ARTIST_ID} = ? ", + additionalArgsBeforeJoins = arrayOf(id), + ) + fun valuesMappedAsFlow( songStore: SongStore, id: String, diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistSongMappingStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistSongMappingStore.kt index 9c42c466..6b531f77 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistSongMappingStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistSongMappingStore.kt @@ -2,8 +2,11 @@ package io.github.zyrouge.symphony.services.database.store import androidx.room.Dao import androidx.room.Insert +import androidx.room.MapColumn import androidx.room.RawQuery +import androidx.room.Update import androidx.sqlite.db.SimpleSQLiteQuery +import androidx.sqlite.db.SupportSQLiteQuery import io.github.zyrouge.symphony.services.groove.entities.Playlist import io.github.zyrouge.symphony.services.groove.entities.PlaylistSongMapping import io.github.zyrouge.symphony.services.groove.entities.Song @@ -19,13 +22,27 @@ abstract class PlaylistSongMappingStore { @Insert abstract suspend fun insert(vararg entities: PlaylistSongMapping) + @Update + abstract suspend fun update(vararg entities: PlaylistSongMapping) + + @RawQuery + protected abstract suspend fun delete(query: SimpleSQLiteQuery): Int + + suspend fun delete(playlistId: String, ids: Collection): Int { + val query = "DELETE FROM ${PlaylistSongMapping.TABLE} " + + "WHERE ${PlaylistSongMapping.COLUMN_PLAYLIST_ID} = ? " + + "AND ${PlaylistSongMapping.COLUMN_ID} IN (${sqlqph(ids.size)})" + val args = arrayOf(playlistId, *ids.toTypedArray()) + return delete(SimpleSQLiteQuery(query, args)) + } + @RawQuery - abstract suspend fun deletePlaylistIds(query: SimpleSQLiteQuery): Int + abstract suspend fun deleteAll(query: SimpleSQLiteQuery): Int - suspend fun deletePlaylistIds(vararg ids: String): Int { + suspend fun deleteAll(vararg playlistIds: String): Int { val query = "DELETE FROM ${PlaylistSongMapping.TABLE} " + - "WHERE ${PlaylistSongMapping.COLUMN_PLAYLIST_ID} IN (${sqlqph(ids.size)})" - return deletePlaylistIds(SimpleSQLiteQuery(query, ids)) + "WHERE ${PlaylistSongMapping.COLUMN_PLAYLIST_ID} IN (${sqlqph(playlistIds.size)})" + return deleteAll(SimpleSQLiteQuery(query, playlistIds)) } @RawQuery(observedEntities = [SongArtworkIndex::class, PlaylistSongMapping::class]) @@ -56,6 +73,98 @@ abstract class PlaylistSongMappingStore { return findSongIdsByPlaylistInternalIdAsFlowRaw(SimpleSQLiteQuery(query, args)) } + @RawQuery + protected abstract fun findById(query: SupportSQLiteQuery): Song.AlongPlaylistMapping? + + fun findById(playlistId: String, id: String?): Song.AlongPlaylistMapping? { + val query = "SELECT ${Song.TABLE}.*, " + + "${PlaylistSongMapping.TABLE}.* " + + "FROM ${PlaylistSongMapping.TABLE} " + + "WHERE ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_PLAYLIST_ID} = ? AND ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_ID} = ? " + + "LEFT JOIN ${Song.TABLE} ON ${Song.TABLE}.${Song.COLUMN_ID} = ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_SONG_ID} " + val args = arrayOf(playlistId, id) + return findById(SimpleSQLiteQuery(query, args)) + } + + @RawQuery + protected abstract fun findByNextId(query: SupportSQLiteQuery): Song.AlongPlaylistMapping? + + fun findByNextId(playlistId: String, nextId: String?): Song.AlongPlaylistMapping? { + val query = "SELECT ${Song.TABLE}.*, " + + "${PlaylistSongMapping.TABLE}.* " + + "FROM ${PlaylistSongMapping.TABLE} " + + "WHERE ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_PLAYLIST_ID} = ? AND ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_NEXT_ID} = ? " + + "LEFT JOIN ${Song.TABLE} ON ${Song.TABLE}.${Song.COLUMN_ID} = ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_SONG_ID} " + val args = arrayOf(playlistId, nextId) + return findByNextId(SimpleSQLiteQuery(query, args)) + } + + @RawQuery + protected abstract fun findHead(query: SupportSQLiteQuery): Song.AlongPlaylistMapping? + + fun findHead(playlistId: String): Song.AlongPlaylistMapping? { + val query = "SELECT ${Song.TABLE}.*, " + + "${PlaylistSongMapping.TABLE}.* " + + "FROM ${PlaylistSongMapping.TABLE} " + + "WHERE ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_PLAYLIST_ID} = ? AND ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_IS_HEAD} = true " + + "LEFT JOIN ${Song.TABLE} ON ${Song.TABLE}.${Song.COLUMN_ID} = ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_SONG_ID} " + val args = arrayOf(playlistId) + return findHead(SimpleSQLiteQuery(query, args)) + } + + @RawQuery + protected abstract fun entriesByIds(query: SupportSQLiteQuery): Map< + @MapColumn(PlaylistSongMapping.COLUMN_ID) String, Song.AlongPlaylistMapping> + + fun entriesByIds(playlistId: String, songMappingIds: List): Map< + String, Song.AlongPlaylistMapping> { + val query = "SELECT ${Song.TABLE}.*, " + + "${PlaylistSongMapping.TABLE}.* " + + "FROM ${PlaylistSongMapping.TABLE} " + + "WHERE ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_PLAYLIST_ID} = ? " + + "AND ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_ID} " + + "IN (${sqlqph(songMappingIds.size)}) " + + "LEFT JOIN ${Song.TABLE} ON ${Song.TABLE}.${Song.COLUMN_ID} = ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_SONG_ID} " + + "ORDER BY ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_IS_HEAD} DESC" + val args = arrayOf(playlistId, *songMappingIds.toTypedArray()) + return entriesByIds(SimpleSQLiteQuery(query, args)) + } + + @RawQuery + protected abstract fun entriesByNextIds(query: SupportSQLiteQuery): Map< + @MapColumn(PlaylistSongMapping.COLUMN_NEXT_ID) String, Song.AlongPlaylistMapping> + + fun entriesByNextIds(playlistId: String, songMappingIds: List): Map< + String, Song.AlongPlaylistMapping> { + val query = "SELECT ${Song.TABLE}.*, " + + "${PlaylistSongMapping.TABLE}.* " + + "FROM ${PlaylistSongMapping.TABLE} " + + "WHERE ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_PLAYLIST_ID} = ? " + + "AND ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_NEXT_ID} " + + "IN (${sqlqph(songMappingIds.size)}) " + + "LEFT JOIN ${Song.TABLE} ON ${Song.TABLE}.${Song.COLUMN_ID} = ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_SONG_ID} " + + "ORDER BY ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_IS_HEAD} DESC" + val args = arrayOf(playlistId, *songMappingIds.toTypedArray()) + return entriesByNextIds(SimpleSQLiteQuery(query, args)) + } + + fun valuesMapped( + songStore: SongStore, + id: String, + sortBy: SongRepository.SortBy, + sortReverse: Boolean, + ): List { + val query = songStore.valuesQuery( + sortBy, + sortReverse, + additionalClauseBeforeJoins = "JOIN ${PlaylistSongMapping.TABLE} ON ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_PLAYLIST_ID} = ?" + + "AND ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_SONG_ID} = ${Song.COLUMN_ID} ", + additionalArgsBeforeJoins = arrayOf(id), + ) + val entries = songStore.entriesAsPlaylistSongMapped(query) + return transformEntriesAsValues(entries) + } + @OptIn(ExperimentalCoroutinesApi::class) fun valuesMappedAsFlow( songStore: SongStore, @@ -70,7 +179,7 @@ abstract class PlaylistSongMappingStore { "AND ${PlaylistSongMapping.TABLE}.${PlaylistSongMapping.COLUMN_SONG_ID} = ${Song.COLUMN_ID} ", additionalArgsBeforeJoins = arrayOf(id), ) - val entries = songStore.entriesAsPlaylistSongMappedAsFlowRaw(query) + val entries = songStore.entriesAsPlaylistSongMappedAsFlow(query) return entries.mapLatest { transformEntriesAsValues(it) } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongStore.kt b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongStore.kt index 6c104602..17b32771 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongStore.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongStore.kt @@ -66,6 +66,9 @@ abstract class SongStore { ) protected abstract fun entriesAsFlow(query: SupportSQLiteQuery): Flow> + internal abstract fun entriesAsPlaylistSongMapped(query: SupportSQLiteQuery): Map< + @MapColumn(Song.COLUMN_ID) String, Song.AlongPlaylistMapping> + @RawQuery( observedEntities = [ AlbumSongMapping::class, @@ -76,7 +79,7 @@ abstract class SongStore { Song::class, ] ) - internal abstract fun entriesAsPlaylistSongMappedAsFlowRaw(query: SupportSQLiteQuery): Flow< + internal abstract fun entriesAsPlaylistSongMappedAsFlow(query: SupportSQLiteQuery): Flow< Map<@MapColumn(Song.COLUMN_ID) String, Song.AlongPlaylistMapping>> fun valuesQuery( @@ -86,6 +89,7 @@ abstract class SongStore { additionalClauseBeforeJoins: String = "", additionalArgsBeforeJoins: Array = emptyArray(), overrideOrderBy: String? = null, + limit: Int? = null, ): SupportSQLiteQuery { val aliasFirstAlbumArtist = "firstAlbumArtist" val embeddedFirstArtistName = "firstArtistName" @@ -134,7 +138,8 @@ abstract class SongStore { "LEFT JOIN ${Album.TABLE} ON ${Album.TABLE}.${Album.COLUMN_ID} = ($albumQuery)" + "LEFT JOIN ${Artist.TABLE} $aliasFirstAlbumArtist ON ${Artist.TABLE}.${Artist.COLUMN_ID} = ($albumArtistQuery)" + "LEFT JOIN ${Composer.TABLE} ON ${Composer.TABLE}.${Composer.COLUMN_ID} = ($composerQuery)" + - "ORDER BY $orderBy $orderDirection" + "ORDER BY $orderBy $orderDirection" + + (if (limit != null) " LIMIT $limit" else "") val args = additionalArgsBeforeJoins return SimpleSQLiteQuery(query, args) } @@ -180,6 +185,7 @@ abstract class SongStore { additionalClauseBeforeJoins: String = "", additionalArgsBeforeJoins: Array = emptyArray(), overrideOrderBy: String? = null, + limit: Int? = null, ): Flow> { val query = valuesQuery( sortBy = sortBy, @@ -188,6 +194,7 @@ abstract class SongStore { additionalClauseBeforeJoins = additionalClauseBeforeJoins, additionalArgsBeforeJoins = additionalArgsBeforeJoins, overrideOrderBy = overrideOrderBy, + limit = limit, ) return valuesAsFlow(query) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/MediaExposer.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/MediaExposer.kt index 8e0630a8..5eec7a84 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/MediaExposer.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/MediaExposer.kt @@ -96,7 +96,7 @@ class MediaExposer(private val symphony: Symphony) { id = symphony.database.playlistSongMappingIdGenerator.next(), playlistId = playlistId, songId = null, - songPath = x, + rawSongPath = x, isHead = i == 0, nextId = nextPlaylistSongMapping?.id, ) @@ -105,7 +105,7 @@ class MediaExposer(private val symphony: Symphony) { } } symphony.database.playlists.update(*playlistsToBeUpdated.toTypedArray()) - symphony.database.playlistSongMapping.deletePlaylistIds(*playlistIdsToBeDeletedInMapping.toTypedArray()) + symphony.database.playlistSongMapping.deleteAll(*playlistIdsToBeDeletedInMapping.toTypedArray()) symphony.database.playlistSongMapping.insert(*playlistSongMappingToBeInserted.toTypedArray()) } catch (err: Exception) { Logger.error("MediaExposer", "playlist fetch failed", err) diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/PlaylistSongMapping.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/PlaylistSongMapping.kt index 5d2df0e8..f79e50e9 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/PlaylistSongMapping.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/entities/PlaylistSongMapping.kt @@ -23,12 +23,6 @@ import androidx.room.PrimaryKey childColumns = arrayOf(PlaylistSongMapping.COLUMN_SONG_ID), onDelete = ForeignKey.SET_NULL, ), - ForeignKey( - entity = Song::class, - parentColumns = arrayOf(Song.COLUMN_PATH), - childColumns = arrayOf(PlaylistSongMapping.COLUMN_SONG_PATH), - onDelete = ForeignKey.SET_NULL, - ), ForeignKey( entity = PlaylistSongMapping::class, parentColumns = arrayOf(PlaylistSongMapping.COLUMN_ID), @@ -39,7 +33,7 @@ import androidx.room.PrimaryKey indices = [ Index(PlaylistSongMapping.COLUMN_PLAYLIST_ID), Index(PlaylistSongMapping.COLUMN_SONG_ID), - Index(PlaylistSongMapping.COLUMN_SONG_PATH), + Index(PlaylistSongMapping.COLUMN_RAW_SONG_PATH), Index(PlaylistSongMapping.COLUMN_IS_HEAD), Index(PlaylistSongMapping.COLUMN_NEXT_ID), ], @@ -52,8 +46,8 @@ data class PlaylistSongMapping( val playlistId: String, @ColumnInfo(COLUMN_SONG_ID) val songId: String?, - @ColumnInfo(COLUMN_SONG_PATH) - val songPath: String?, + @ColumnInfo(COLUMN_RAW_SONG_PATH) + val rawSongPath: String?, @ColumnInfo(COLUMN_IS_HEAD) val isHead: Boolean, @ColumnInfo(COLUMN_NEXT_ID) @@ -64,7 +58,7 @@ data class PlaylistSongMapping( const val COLUMN_ID = "mapping_id" const val COLUMN_PLAYLIST_ID = "playlist_id" const val COLUMN_SONG_ID = "song_id" - const val COLUMN_SONG_PATH = "song_path" + const val COLUMN_RAW_SONG_PATH = "raw_song_path" const val COLUMN_IS_HEAD = "is_head" const val COLUMN_NEXT_ID = "next_id" } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumRepository.kt index 1dd2e918..4e479b2e 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/AlbumRepository.kt @@ -22,6 +22,14 @@ class AlbumRepository(private val symphony: Symphony) { albumId = id, ) + fun findSongsById(id: String, sortBy: SongRepository.SortBy, sortReverse: Boolean) = + symphony.database.albumSongMapping.valuesMapped( + symphony.database.songs, + id, + sortBy, + sortReverse + ) + fun findSongsByIdAsFlow(id: String, sortBy: SongRepository.SortBy, sortReverse: Boolean) = symphony.database.albumSongMapping.valuesMappedAsFlow( symphony.database.songs, @@ -30,6 +38,13 @@ class AlbumRepository(private val symphony: Symphony) { sortReverse ) + @OptIn(ExperimentalCoroutinesApi::class) + fun getArtworkUriAsFlow(id: String) = + symphony.database.albumSongMapping.findTop4SongArtworksAsFlow(id) + .mapLatest { indices -> + indices.map { symphony.groove.song.getArtworkUriFromIndex(it) } + } + @OptIn(ExperimentalCoroutinesApi::class) fun getTop4ArtworkUriAsFlow(id: String) = symphony.database.albumSongMapping.findTop4SongArtworksAsFlow(id) diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/ArtistRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/ArtistRepository.kt index 0a111ffa..56992c0c 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/ArtistRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/ArtistRepository.kt @@ -20,6 +20,14 @@ class ArtistRepository(private val symphony: Symphony) { artistId = id, ) + fun findSongsById(id: String, sortBy: SongRepository.SortBy, sortReverse: Boolean) = + symphony.database.artistSongMapping.valuesMapped( + symphony.database.songs, + id, + sortBy, + sortReverse + ) + fun findSongsByIdAsFlow(id: String, sortBy: SongRepository.SortBy, sortReverse: Boolean) = symphony.database.artistSongMapping.valuesMappedAsFlow( symphony.database.songs, diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/PlaylistRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/PlaylistRepository.kt index c734cfa7..042b3b02 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/PlaylistRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/PlaylistRepository.kt @@ -1,8 +1,14 @@ package io.github.zyrouge.symphony.services.groove.repositories +import android.net.Uri +import androidx.room.withTransaction import io.github.zyrouge.symphony.Symphony +import io.github.zyrouge.symphony.services.database.PersistentDatabase +import io.github.zyrouge.symphony.services.database.store.PlaylistSongMappingStore import io.github.zyrouge.symphony.services.groove.entities.Playlist import io.github.zyrouge.symphony.services.groove.entities.PlaylistSongMapping +import io.github.zyrouge.symphony.services.groove.entities.Song +import io.github.zyrouge.symphony.utils.lazy_linked_list.LazyLinkedListOperator import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.mapLatest @@ -15,6 +21,60 @@ class PlaylistRepository(private val symphony: Symphony) { TRACKS_COUNT, } + private class PlaylistSongMappingOperatorEntityFunctions : + LazyLinkedListOperator.EntityFunctions { + override fun getEntityId(entity: PlaylistSongMapping) = entity.id + override fun getEntityNextId(entity: PlaylistSongMapping) = entity.nextId + override fun getEntityIsHead(entity: PlaylistSongMapping) = entity.isHead + + override fun updateEntityNextId(entity: PlaylistSongMapping, nNextId: String?) = + entity.copy(nextId = nNextId) + + override fun updateEntityIsHead(entity: PlaylistSongMapping, nIsHead: Boolean) = + entity.copy(isHead = nIsHead) + } + + private class PlaylistSongMappingOperatorPersistenceFunctions( + private val persistentDatabase: PersistentDatabase, + private val store: PlaylistSongMappingStore, + private val playlistId: String, + ) : + LazyLinkedListOperator.PersistenceFunctions { + override fun getEntitiesByIds(ids: List) = store.entriesByIds(playlistId, ids) + .mapValues { it.value.mapping } + + override fun getEntitiesByNextIds(nextIds: List) = + store.entriesByNextIds(playlistId, nextIds) + .mapValues { it.value.mapping } + + override fun getHeadEntity() = store.findHead(playlistId)?.mapping + override fun getTailEntity() = store.findByNextId(playlistId, null)?.mapping + + override suspend fun saveEntities( + addedEntities: List, + modifiedEntities: List, + deletedKeys: List, + ) { + if (addedEntities.isEmpty() || modifiedEntities.isEmpty() || deletedKeys.isEmpty()) { + return + } + persistentDatabase.withTransaction { + if (addedEntities.isNotEmpty()) { + store.insert(*addedEntities.toTypedArray()) + } + if (modifiedEntities.isNotEmpty()) { + store.update(*modifiedEntities.toTypedArray()) + } + if (deletedKeys.isNotEmpty()) { + store.delete(playlistId, deletedKeys) + } + } + } + } + + private interface PlaylistSongMappingOperatorDataChangeFunctions : + LazyLinkedListOperator.DataChangeFunctions + private lateinit var favoriteSongIdsFlow: Flow> private var favoriteSongIds = emptyList() @@ -22,35 +82,72 @@ class PlaylistRepository(private val symphony: Symphony) { observeFavoritesPlaylistChanges() } - data class AddOptions( - val playlist: Playlist, - val songIds: List = emptyList(), - val songPaths: List = emptyList(), - ) + suspend fun create(fn: (id: String) -> Playlist): Playlist { + val playlist = fn(symphony.database.playlistsIdGenerator.next()) + symphony.database.playlists.insert(playlist) + return playlist + } - fun add(options: AddOptions) { - val mappings = mutableListOf() - var nextId: String? = null - for (i in (options.songPaths.size - 1) downTo 0) { - val mapping = PlaylistSongMapping( - id = symphony.database.playlistSongMappingIdGenerator.next(), - playlistId = options.playlist.id, - songId = null, - songPath = options.songPaths[i], - isHead = i == 0, - nextId = nextId, - ) - mappings.add(mapping) - nextId = mapping.id - } - symphony.groove.coroutineScope.launch { - symphony.database.playlists.insert(options.playlist) - symphony.database.playlistSongMapping.insert(*mappings.toTypedArray()) + suspend fun save(playlist: Playlist): Boolean { + return symphony.database.playlists.update(playlist) > 0 + } + + sealed class AddPosition { + object BeforeHead : AddPosition() + class After(val id: String) : AddPosition() + object AfterTail : AddPosition() + } + + suspend fun addSongs( + playlistId: String, + songs: List, + position: AddPosition = AddPosition.AfterTail, + ) = addSongs(playlistId, songs.map { it.id }, position) + + suspend fun addSongs( + playlistId: String, + songIds: List, + position: AddPosition = AddPosition.AfterTail, + ): Boolean { + val operator = createPlaylistSongMappingOperator(playlistId) + val changeset = when (position) { + AddPosition.BeforeHead -> operator.prependHead(songIds) { x, isHead, nextId -> + PlaylistSongMapping( + id = symphony.database.playlistSongMappingIdGenerator.next(), + playlistId = playlistId, + songId = x, + rawSongPath = null, + isHead = isHead, + nextId = nextId, + ) + } + + is AddPosition.After, AddPosition.AfterTail -> { + val insertAfterId = if (position is AddPosition.After) position.id else null + operator.append(insertAfterId, songIds) { x, isHead, nextId -> + PlaylistSongMapping( + id = symphony.database.playlistSongMappingIdGenerator.next(), + playlistId = playlistId, + songId = x, + rawSongPath = null, + isHead = isHead, + nextId = nextId, + ) + } + } } + operator.persist(changeset) + return changeset.addedKeys.isNotEmpty() } - fun removeSongs(playlistId: String, songIds: List) { - // TODO: implement this + suspend fun removeSongs(playlistId: String, songs: List) = + removeSongs(playlistId, songs.map { it.id }) + + suspend fun removeSongs(playlistId: String, songIds: List): Boolean { + val operator = createPlaylistSongMappingOperator(playlistId) + val changeset = operator.remove(songIds) + operator.persist(changeset) + return changeset.deletedKeys.isNotEmpty() } fun isFavoriteSong(songId: String) = favoriteSongIds.contains(songId) @@ -62,6 +159,14 @@ class PlaylistRepository(private val symphony: Symphony) { fun findByIdAsFlow(id: String) = symphony.database.playlists.findByIdAsFlow(id) + fun findSongsById(id: String, sortBy: SongRepository.SortBy, sortReverse: Boolean) = + symphony.database.playlistSongMapping.valuesMapped( + symphony.database.songs, + id, + sortBy, + sortReverse + ) + fun findSongsByIdAsFlow(id: String, sortBy: SongRepository.SortBy, sortReverse: Boolean) = symphony.database.playlistSongMapping.valuesMappedAsFlow( symphony.database.songs, @@ -70,6 +175,9 @@ class PlaylistRepository(private val symphony: Symphony) { sortReverse ) + fun findSongById(id: String, songId: String) = + symphony.database.playlistSongMapping.findById(id, songId) + @OptIn(ExperimentalCoroutinesApi::class) fun getTop4ArtworkUriAsFlow(id: String) = symphony.database.playlistSongMapping.findTop4SongArtworksAsFlow(id) @@ -80,6 +188,20 @@ class PlaylistRepository(private val symphony: Symphony) { fun valuesAsFlow(sortBy: SortBy, sortReverse: Boolean) = symphony.database.playlists.valuesAsFlow(sortBy, sortReverse) + suspend fun export(playlist: Playlist, uri: Uri) { + val songs = symphony.database.playlistSongMapping.valuesMapped( + symphony.database.songs, + playlist.id, + SongRepository.SortBy.CUSTOM, + false, + ) + val outputStream = symphony.applicationContext.contentResolver.openOutputStream(uri, "w") + outputStream?.use { + val content = songs.joinToString("\n") { x -> x.path } + it.write(content.toByteArray()) + } + } + private fun observeFavoritesPlaylistChanges() { favoriteSongIdsFlow = symphony.database.playlistSongMapping .findSongIdsByPlaylistInternalIdAsFlow(PLAYLIST_INTERNAL_ID_FAVORITES) @@ -90,6 +212,19 @@ class PlaylistRepository(private val symphony: Symphony) { } } + private fun createPlaylistSongMappingOperator( + playlistId: String, + dataChangeFunctions: PlaylistSongMappingOperatorDataChangeFunctions? = null, + ) = LazyLinkedListOperator( + PlaylistSongMappingOperatorEntityFunctions(), + PlaylistSongMappingOperatorPersistenceFunctions( + symphony.database.persistent, + symphony.database.playlistSongMapping, + playlistId, + ), + dataChangeFunctions, + ) + companion object { const val PLAYLIST_INTERNAL_ID_FAVORITES = 1 } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/SongRepository.kt b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/SongRepository.kt index 4336c238..3166bdf4 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/SongRepository.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/groove/repositories/SongRepository.kt @@ -61,6 +61,6 @@ class SongRepository(private val symphony: Symphony) { symphony.database.songArtworks.get(it).toUri() } - fun valuesAsFlow(sortBy: SortBy, sortReverse: Boolean) = - symphony.database.songs.valuesAsFlow(sortBy, sortReverse) + fun valuesAsFlow(sortBy: SortBy, sortReverse: Boolean, limit: Int? = null) = + symphony.database.songs.valuesAsFlow(sortBy, sortReverse, limit = limit) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/services/radio/Radio.kt b/app/src/main/java/io/github/zyrouge/symphony/services/radio/Radio.kt index ad3a51bf..6b4d9140 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/services/radio/Radio.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/services/radio/Radio.kt @@ -62,6 +62,15 @@ class Radio(private val symphony: Symphony) : SymphonyHooks { return true } + suspend fun playBySongId(songId: String): Boolean { + val songQueue = queue.getCurrentSongQueue() ?: return false + val songQueueId = songQueue.entity.id + val song = symphony.database.songQueueSongMapping.findBySongId(songQueueId, songId) + ?: return false + play(song.mapping.id, song.entity.uri) + return true + } + private suspend fun play( songMappingId: String, songUri: Uri, @@ -209,6 +218,11 @@ class Radio(private val symphony: Symphony) : SymphonyHooks { suspend fun remove(songMappingId: String) = queue.remove(songMappingId) + suspend fun clear(): Boolean { + stop() + return queue.clear() + } + internal fun onQueueCurrentPlayingSongChanged(song: Song.AlongSongQueueMapping?) { symphony.groove.coroutineScope.launch { onQueueCurrentPlayingSongChangedNeedsSuspend(song) diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/AddToPlaylistDialog.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/AddToPlaylistDialog.kt index 04dd9f28..d8cadfc5 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/AddToPlaylistDialog.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/AddToPlaylistDialog.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check import androidx.compose.material3.Icon @@ -16,27 +17,29 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.runtime.toMutableStateList import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.zyrouge.symphony.services.groove.entities.Song +import io.github.zyrouge.symphony.services.groove.repositories.PlaylistRepository import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.utils.builtin.mutate +import io.github.zyrouge.symphony.ui.helpers.createGrooveArtworkImageRequests +import kotlinx.coroutines.launch @Composable fun AddToPlaylistDialog( context: ViewContext, - songIds: List, + songs: List, onDismissRequest: () -> Unit, ) { var showNewPlaylistDialog by remember { mutableStateOf(false) } - val allPlaylistsIds by context.symphony.groove.playlist.valuesAsFlow() - .collectAsStateWithLifecycle() - val playlists by remember(allPlaylistsIds) { + val sortBy by context.symphony.settings.lastUsedPlaylistsSortBy.flow.collectAsStateWithLifecycle() + val sortReverse by context.symphony.settings.lastUsedPlaylistsSortReverse.flow.collectAsStateWithLifecycle() + val allPlaylists by context.symphony.groove.playlist.valuesAsFlow(sortBy, sortReverse) + .collectAsStateWithLifecycle(listOf()) + val playlists by remember(allPlaylists) { derivedStateOf { - allPlaylistsIds - .mapNotNull { context.symphony.groove.playlist.get(it) } - .filter { it.isNotLocal } - .toMutableStateList() + allPlaylists.filter { !it.entity.isLocal } } } @@ -50,14 +53,30 @@ fun AddToPlaylistDialog( playlists.isEmpty() -> SubtleCaptionText(context.symphony.t.NoInAppPlaylistsFound) else -> LazyColumn(modifier = Modifier.padding(bottom = 4.dp)) { items(playlists) { playlist -> - val playlistSongIds = playlist.getSongIds(context.symphony) + val containsSong by remember(songs) { + derivedStateOf { + val found = when { + songs.size == 1 -> context.symphony.groove.playlist.findSongById( + playlist.entity.id, + songs[0].id + ) + + else -> null + } + found != null + } + } + val artworkUris by context.symphony.groove.playlist + .getTop4ArtworkUriAsFlow(playlist.entity.id) + .collectAsStateWithLifecycle(listOf()) GenericGrooveCard( - image = playlist - .createArtworkImageRequest(context.symphony) - .build(), + images = createGrooveArtworkImageRequests( + context.symphony, + artworkUris + ), imageLabel = when { - songIds.size == 1 && playlistSongIds.contains(songIds[0]) -> ({ + containsSong -> ({ Icon( Icons.Filled.Check, null, @@ -68,7 +87,7 @@ fun AddToPlaylistDialog( else -> null }, title = { - Text(playlist.title) + Text(playlist.entity.title) }, options = { expanded, onDismissRequest -> PlaylistDropdownMenu( @@ -79,10 +98,13 @@ fun AddToPlaylistDialog( ) }, onClick = { - context.symphony.groove.playlist.update( - playlist.id, - playlistSongIds.mutate { addAll(songIds) }, - ) + context.symphony.groove.coroutineScope.launch { + context.symphony.groove.playlist.addSongs( + playlist.entity.id, + songs, + PlaylistRepository.AddPosition.AfterTail, + ) + } onDismissRequest() } ) @@ -108,8 +130,10 @@ fun AddToPlaylistDialog( NewPlaylistDialog( context = context, onDone = { playlist -> + context.symphony.groove.coroutineScope.launch { + context.symphony.groove.playlist.addSongs(playlist.id, songs) + } showNewPlaylistDialog = false - context.symphony.groove.playlist.add(playlist) }, onDismissRequest = { showNewPlaylistDialog = false diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumArtistTile.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumArtistTile.kt deleted file mode 100644 index ae280de1..00000000 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumArtistTile.kt +++ /dev/null @@ -1,130 +0,0 @@ -package io.github.zyrouge.symphony.ui.components - -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.PlaylistAdd -import androidx.compose.material.icons.automirrored.filled.PlaylistPlay -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import io.github.zyrouge.symphony.services.groove.AlbumArtist -import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.ui.view.AlbumArtistViewRoute - -@Composable -fun AlbumArtistTile(context: ViewContext, albumArtist: AlbumArtist) { - SquareGrooveTile( - image = albumArtist.createArtworkImageRequest(context.symphony).build(), - options = { expanded, onDismissRequest -> - AlbumArtistDropdownMenu( - context, - albumArtist, - expanded = expanded, - onDismissRequest = onDismissRequest, - ) - }, - content = { - Text( - albumArtist.name, - style = MaterialTheme.typography.bodyMedium, - textAlign = TextAlign.Center, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) - }, - onPlay = { - context.symphony.radio.shorty.playQueue(albumArtist.getSortedSongIds(context.symphony)) - }, - onClick = { - context.navController.navigate(AlbumArtistViewRoute(albumArtist.name)) - } - ) -} - -@Composable -fun AlbumArtistDropdownMenu( - context: ViewContext, - albumArtist: AlbumArtist, - expanded: Boolean, - onDismissRequest: () -> Unit, -) { - var showAddToPlaylistDialog by remember { mutableStateOf(false) } - - DropdownMenu( - expanded = expanded, - onDismissRequest = onDismissRequest - ) { - DropdownMenuItem( - leadingIcon = { - Icon(Icons.AutoMirrored.Filled.PlaylistPlay, null) - }, - text = { - Text(context.symphony.t.ShufflePlay) - }, - onClick = { - onDismissRequest() - context.symphony.radio.shorty.playQueue( - albumArtist.getSortedSongIds(context.symphony), - shuffle = true, - ) - } - ) - DropdownMenuItem( - leadingIcon = { - Icon(Icons.AutoMirrored.Filled.PlaylistPlay, null) - }, - text = { - Text(context.symphony.t.PlayNext) - }, - onClick = { - onDismissRequest() - context.symphony.radio.queue.add( - albumArtist.getSortedSongIds(context.symphony), - context.symphony.radio.queue.currentSongIndex + 1 - ) - } - ) - DropdownMenuItem( - leadingIcon = { - Icon(Icons.AutoMirrored.Filled.PlaylistPlay, null) - }, - text = { - Text(context.symphony.t.AddToQueue) - }, - onClick = { - onDismissRequest() - context.symphony.radio.queue.add(albumArtist.getSortedSongIds(context.symphony)) - } - ) - DropdownMenuItem( - leadingIcon = { - Icon(Icons.AutoMirrored.Filled.PlaylistAdd, null) - }, - text = { - Text(context.symphony.t.AddToPlaylist) - }, - onClick = { - onDismissRequest() - showAddToPlaylistDialog = true - } - ) - } - - if (showAddToPlaylistDialog) { - AddToPlaylistDialog( - context, - songIds = albumArtist.getSongIds(context.symphony), - onDismissRequest = { - showAddToPlaylistDialog = false - } - ) - } -} diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumGrid.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumGrid.kt index 9cb8d769..6bf1c16a 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumGrid.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumGrid.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.zyrouge.symphony.services.groove.Groove import io.github.zyrouge.symphony.services.groove.entities.Album import io.github.zyrouge.symphony.services.groove.repositories.AlbumRepository diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumTile.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumTile.kt index 081b3871..b48a97f9 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumTile.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumTile.kt @@ -16,15 +16,23 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow -import io.github.zyrouge.symphony.services.groove.Album +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.zyrouge.symphony.services.groove.entities.Album import io.github.zyrouge.symphony.ui.helpers.ViewContext +import io.github.zyrouge.symphony.ui.helpers.createGrooveArtworkImageRequests import io.github.zyrouge.symphony.ui.view.AlbumViewRoute import io.github.zyrouge.symphony.ui.view.ArtistViewRoute +import kotlinx.coroutines.launch @Composable fun AlbumTile(context: ViewContext, album: Album) { + val artworkUris by context.symphony.groove.album.getTop4ArtworkUriAsFlow(album.id) + .collectAsStateWithLifecycle(listOf()) + val artists by context.symphony.groove.album.findArtistsOfIdAsFlow(album.id) + .collectAsStateWithLifecycle(listOf()) + SquareGrooveTile( - image = album.createArtworkImageRequest(context.symphony).build(), + images = createGrooveArtworkImageRequests(context.symphony, artworkUris), options = { expanded, onDismissRequest -> AlbumDropdownMenu( context, @@ -41,9 +49,9 @@ fun AlbumTile(context: ViewContext, album: Album) { maxLines = 2, overflow = TextOverflow.Ellipsis, ) - if (album.artists.isNotEmpty()) { + if (artists.isNotEmpty()) { Text( - album.artists.joinToString(), + artists.joinToString { it.entity.name }, style = MaterialTheme.typography.bodySmall, textAlign = TextAlign.Center, maxLines = 2, @@ -52,7 +60,16 @@ fun AlbumTile(context: ViewContext, album: Album) { } }, onPlay = { - context.symphony.radio.shorty.playQueue(album.getSortedSongIds(context.symphony)) + context.symphony.groove.coroutineScope.launch { + val songs = context.symphony.groove.album.findSongsById( + album.id, + context.symphony.settings.lastUsedAlbumSongsSortBy.value, + context.symphony.settings.lastUsedAlbumSongsSortReverse.value + ) + context.symphony.radio.clear() + context.symphony.radio.add(songs) + context.symphony.radio.play() + } }, onClick = { context.navController.navigate(AlbumViewRoute(album.id)) @@ -67,6 +84,8 @@ fun AlbumDropdownMenu( expanded: Boolean, onDismissRequest: () -> Unit, ) { + val artists by context.symphony.groove.album.findArtistsOfIdAsFlow(album.id) + .collectAsStateWithLifecycle(listOf()) var showAddToPlaylistDialog by remember { mutableStateOf(false) } DropdownMenu( @@ -81,11 +100,18 @@ fun AlbumDropdownMenu( Text(context.symphony.t.ShufflePlay) }, onClick = { + context.symphony.groove.coroutineScope.launch { + val songs = context.symphony.groove.album.findSongsById( + album.id, + context.symphony.settings.lastUsedAlbumSongsSortBy.value, + context.symphony.settings.lastUsedAlbumSongsSortReverse.value + ) + context.symphony.radio.clear() + context.symphony.radio.add(songs) + context.symphony.radio.setShuffleMode(true) + context.symphony.radio.play() + } onDismissRequest() - context.symphony.radio.shorty.playQueue( - album.getSortedSongIds(context.symphony), - shuffle = true, - ) } ) DropdownMenuItem( @@ -96,11 +122,8 @@ fun AlbumDropdownMenu( Text(context.symphony.t.PlayNext) }, onClick = { + // TODO onDismissRequest() - context.symphony.radio.queue.add( - album.getSortedSongIds(context.symphony), - context.symphony.radio.queue.currentSongIndex + 1 - ) } ) DropdownMenuItem( @@ -111,8 +134,16 @@ fun AlbumDropdownMenu( Text(context.symphony.t.AddToQueue) }, onClick = { + context.symphony.groove.coroutineScope.launch { + val songs = context.symphony.groove.album.findSongsById( + album.id, + context.symphony.settings.lastUsedAlbumSongsSortBy.value, + context.symphony.settings.lastUsedAlbumSongsSortReverse.value + ) + context.symphony.radio.add(songs) + context.symphony.radio.play() + } onDismissRequest() - context.symphony.radio.queue.add(album.getSortedSongIds(context.symphony)) } ) DropdownMenuItem( @@ -127,26 +158,34 @@ fun AlbumDropdownMenu( showAddToPlaylistDialog = true } ) - album.artists.forEach { artistName -> + artists.forEach { DropdownMenuItem( leadingIcon = { Icon(Icons.Filled.Person, null) }, text = { - Text("${context.symphony.t.ViewArtist}: $artistName") + Text("${context.symphony.t.ViewArtist}: ${it.entity.name}") }, onClick = { onDismissRequest() - context.navController.navigate(ArtistViewRoute(artistName)) + context.navController.navigate(ArtistViewRoute(it.entity.name)) } ) } } if (showAddToPlaylistDialog) { + val songs = remember { + context.symphony.groove.album.findSongsById( + album.id, + context.symphony.settings.lastUsedAlbumSongsSortBy.value, + context.symphony.settings.lastUsedAlbumSongsSortReverse.value + ) + } + AddToPlaylistDialog( context, - songIds = album.getSongIds(context.symphony), + songs = songs, onDismissRequest = { showAddToPlaylistDialog = false } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/ArtistTile.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/ArtistTile.kt index 7060469f..89d25d43 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/ArtistTile.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/ArtistTile.kt @@ -15,14 +15,20 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow -import io.github.zyrouge.symphony.services.groove.Artist +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.zyrouge.symphony.services.groove.entities.Artist import io.github.zyrouge.symphony.ui.helpers.ViewContext +import io.github.zyrouge.symphony.ui.helpers.createGrooveArtworkImageRequests import io.github.zyrouge.symphony.ui.view.ArtistViewRoute +import kotlinx.coroutines.launch @Composable fun ArtistTile(context: ViewContext, artist: Artist) { + val artworkUris by context.symphony.groove.artist.getTop4ArtworkUriAsFlow(artist.id) + .collectAsStateWithLifecycle(listOf()) + SquareGrooveTile( - image = artist.createArtworkImageRequest(context.symphony).build(), + images = createGrooveArtworkImageRequests(context.symphony, artworkUris), options = { expanded, onDismissRequest -> ArtistDropdownMenu( context, @@ -41,7 +47,16 @@ fun ArtistTile(context: ViewContext, artist: Artist) { ) }, onPlay = { - context.symphony.radio.shorty.playQueue(artist.getSortedSongIds(context.symphony)) + context.symphony.groove.coroutineScope.launch { + val songs = context.symphony.groove.artist.findSongsById( + artist.id, + context.symphony.settings.lastUsedArtistSongsSortBy.value, + context.symphony.settings.lastUsedArtistSongsSortReverse.value + ) + context.symphony.radio.clear() + context.symphony.radio.add(songs) + context.symphony.radio.play() + } }, onClick = { context.navController.navigate(ArtistViewRoute(artist.name)) @@ -70,11 +85,18 @@ fun ArtistDropdownMenu( Text(context.symphony.t.ShufflePlay) }, onClick = { + context.symphony.groove.coroutineScope.launch { + val songs = context.symphony.groove.artist.findSongsById( + artist.id, + context.symphony.settings.lastUsedArtistSongsSortBy.value, + context.symphony.settings.lastUsedArtistSongsSortReverse.value + ) + context.symphony.radio.clear() + context.symphony.radio.add(songs) + context.symphony.radio.setShuffleMode(true) + context.symphony.radio.play() + } onDismissRequest() - context.symphony.radio.shorty.playQueue( - artist.getSortedSongIds(context.symphony), - shuffle = true - ) } ) DropdownMenuItem( @@ -85,11 +107,8 @@ fun ArtistDropdownMenu( Text(context.symphony.t.PlayNext) }, onClick = { + // TODO onDismissRequest() - context.symphony.radio.queue.add( - artist.getSortedSongIds(context.symphony), - context.symphony.radio.queue.currentSongIndex + 1 - ) } ) DropdownMenuItem( @@ -100,8 +119,16 @@ fun ArtistDropdownMenu( Text(context.symphony.t.AddToQueue) }, onClick = { + context.symphony.groove.coroutineScope.launch { + val songs = context.symphony.groove.artist.findSongsById( + artist.id, + context.symphony.settings.lastUsedArtistSongsSortBy.value, + context.symphony.settings.lastUsedArtistSongsSortReverse.value + ) + context.symphony.radio.add(songs) + context.symphony.radio.play() + } onDismissRequest() - context.symphony.radio.queue.add(artist.getSortedSongIds(context.symphony)) } ) DropdownMenuItem( @@ -119,9 +146,17 @@ fun ArtistDropdownMenu( } if (showAddToPlaylistDialog) { + val songs = remember { + context.symphony.groove.artist.findSongsById( + artist.id, + context.symphony.settings.lastUsedArtistSongsSortBy.value, + context.symphony.settings.lastUsedArtistSongsSortReverse.value + ) + } + AddToPlaylistDialog( context, - songIds = artist.getSongIds(context.symphony), + songs = songs, onDismissRequest = { showAddToPlaylistDialog = false } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenericGrooveArtworkGrid.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenericGrooveArtworkGrid.kt new file mode 100644 index 00000000..d9d5ddda --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenericGrooveArtworkGrid.kt @@ -0,0 +1,65 @@ +package io.github.zyrouge.symphony.ui.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun GenericGrooveArtworkGrid(images: List) { + val imagesCount = images.size + + when { + imagesCount == 0 -> { + // TODO + } + + images.size == 1 -> Box { + AsyncImage( + images.first(), + null, + modifier = Modifier + .size(45.dp) + .clip(RoundedCornerShape(10.dp)), + ) + } + + else -> LazyVerticalGrid( + columns = GridCells.Fixed(2), + modifier = Modifier + .size(45.dp) + .clip(RoundedCornerShape(10.dp)), + ) { + items(4) { i -> + val x = images.getOrNull(i) + + AsyncImage( + it, + null, + contentScale = ContentScale.Crop, + ) + } + } + } + images?.let { + Box { + AsyncImage( + it, + null, + modifier = Modifier + .size(45.dp) + .clip(RoundedCornerShape(10.dp)), + ) + } + } +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenericGrooveBanner.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenericGrooveBanner.kt index dd704558..5f7d115f 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenericGrooveBanner.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenericGrooveBanner.kt @@ -32,7 +32,7 @@ import coil.compose.AsyncImage import io.github.zyrouge.symphony.ui.helpers.Assets import io.github.zyrouge.symphony.ui.helpers.ScreenOrientation import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.ui.helpers.createHandyImageRequest +import io.github.zyrouge.symphony.ui.helpers.createHandyImageRequestBuilder @Composable fun GenericGrooveBanner( @@ -131,7 +131,7 @@ fun GenericGrooveBannerQuadImage( } private fun createGrooveImageRequest(context: ViewContext, uri: Uri?) { - createHandyImageRequest( + createHandyImageRequestBuilder( context.symphony.applicationContext, uri ?: Assets.getPlaceholderUri(context.symphony), Assets.getPlaceholderId(context.symphony), diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenericGrooveCard.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenericGrooveCard.kt index 1b5bf07c..22c7213a 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenericGrooveCard.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenericGrooveCard.kt @@ -36,7 +36,9 @@ import coil.request.ImageRequest @OptIn(ExperimentalMaterial3Api::class) @Composable fun GenericGrooveCard( - image: ImageRequest?, + image: ImageRequest? = null, + // TODO + images: List? = null, imageLabel: (@Composable () -> Unit)? = null, title: @Composable () -> Unit, subtitle: (@Composable () -> Unit)? = null, diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenericSongListDropdown.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenericSongListDropdown.kt index 5547a132..a9bc40f7 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenericSongListDropdown.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenericSongListDropdown.kt @@ -83,7 +83,7 @@ fun GenericSongListDropdown( if (showAddToPlaylistDialog) { AddToPlaylistDialog( context, - songIds = songIds, + songs = songIds, onDismissRequest = { showAddToPlaylistDialog = false } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/IntroductoryDialog.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/IntroductoryDialog.kt index e1343216..1642fb0f 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/IntroductoryDialog.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/IntroductoryDialog.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.zyrouge.symphony.ui.helpers.ViewContext @Composable diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/NewPlaylistDialog.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/NewPlaylistDialog.kt index a198009c..1cf38aa5 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/NewPlaylistDialog.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/NewPlaylistDialog.kt @@ -23,20 +23,22 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import io.github.zyrouge.symphony.services.groove.entities.Playlist -import io.github.zyrouge.symphony.services.groove.repositories.PlaylistRepository +import io.github.zyrouge.symphony.services.groove.entities.Song import io.github.zyrouge.symphony.ui.helpers.ViewContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext @Composable fun NewPlaylistDialog( context: ViewContext, - initialSongIds: List = listOf(), - onDone: (PlaylistRepository.AddOptions) -> Unit, + initialSongs: List = listOf(), + onDone: (Playlist) -> Unit, onDismissRequest: () -> Unit, ) { var input by remember { mutableStateOf("") } var showSongsPicker by remember { mutableStateOf(false) } - val songIds = remember { mutableStateListOf(*initialSongIds.toTypedArray()) } - val songIdsImmutable = songIds.toList() + val songs = remember { mutableStateListOf(*initialSongs.toTypedArray()) } val focusRequester = remember { FocusRequester() } LaunchedEffect(LocalContext.current) { @@ -76,23 +78,28 @@ fun NewPlaylistDialog( showSongsPicker = true } ) { - Text(context.symphony.t.AddSongs + " (${songIds.size})") + Text(context.symphony.t.AddSongs + " (${songs.size})") } Spacer(modifier = Modifier.weight(1f)) TextButton( enabled = input.isNotBlank(), onClick = { - val playlist = Playlist( - id = context.symphony.database.playlistsIdGenerator.next(), - title = input, - uri = null, - path = null, - ) - val addOptions = PlaylistRepository.AddOptions( - playlist = playlist, - songIds = songIds.toList(), - ) - onDone(addOptions) + context.symphony.groove.coroutineScope.launch { + val playlist = context.symphony.groove.playlist.create { id -> + Playlist( + id = id, + title = input, + uri = null, + path = null, + ) + } + if (songs.isNotEmpty()) { + context.symphony.groove.playlist.addSongs(playlist.id, songs) + } + withContext(Dispatchers.Main) { + onDone(playlist) + } + } } ) { Text(context.symphony.t.Done) diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/PlaylistTile.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/PlaylistTile.kt index d3c29799..1f031d95 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/PlaylistTile.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/PlaylistTile.kt @@ -43,18 +43,19 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.compose.AsyncImage import io.github.zyrouge.symphony.services.groove.MediaExposer import io.github.zyrouge.symphony.services.groove.entities.Playlist -import io.github.zyrouge.symphony.services.groove.entities.Song import io.github.zyrouge.symphony.ui.helpers.ViewContext import io.github.zyrouge.symphony.ui.theme.ThemeColors import io.github.zyrouge.symphony.ui.view.PlaylistViewRoute import io.github.zyrouge.symphony.utils.Logger +import kotlinx.coroutines.launch @Composable fun PlaylistTile(context: ViewContext, playlist: Playlist) { - val artworks by context.symphony.groove.playlist.getTop4ArtworkUriAsFlow(playlist.id) + val artworkUris by context.symphony.groove.playlist.getTop4ArtworkUriAsFlow(playlist.id) .collectAsStateWithLifecycle(emptyList()) Card( @@ -71,7 +72,7 @@ fun PlaylistTile(context: ViewContext, playlist: Playlist) { Box { AsyncImage( // TODO: remove this hack after moving to reactive objects - artworks.first(), + artworkUris.first(), null, contentScale = ContentScale.Crop, modifier = Modifier @@ -113,9 +114,9 @@ fun PlaylistTile(context: ViewContext, playlist: Playlist) { ) .then(Modifier.size(36.dp)), onClick = { - context.symphony.radio.shorty.playQueue( - playlist.getSortedSongIds(context.symphony) - ) +// context.symphony.radio.shorty.playQueue( +// playlist.getSortedSongIds(context.symphony) +// ) } ) { Icon(Icons.Filled.PlayArrow, null) @@ -136,32 +137,32 @@ fun PlaylistTile(context: ViewContext, playlist: Playlist) { @Composable fun PlaylistDropdownMenu( context: ViewContext, - playlist: Playlist.AlongAttributes, - songs: List, + playlist: Playlist, expanded: Boolean, - onDelete: () -> Unit, onDismissRequest: () -> Unit, ) { val savePlaylistLauncher = rememberLauncherForActivityResult( ActivityResultContracts.CreateDocument(MediaExposer.MIMETYPE_M3U) ) { uri -> uri?.let { _ -> - try { - context.symphony.groove.playlist.savePlaylistToUri(playlist, uri) - Toast.makeText( - context.activity, - context.symphony.t.ExportedX(playlist.title), - Toast.LENGTH_SHORT, - ).show() - } catch (err: Exception) { - Logger.error("PlaylistTile", "export failed (activity result)", err) - Toast.makeText( - context.activity, - context.symphony.t.ExportFailedX( - err.localizedMessage ?: err.toString() - ), - Toast.LENGTH_SHORT, - ).show() + context.symphony.groove.coroutineScope.launch { + try { + context.symphony.groove.playlist.export(playlist, uri) + Toast.makeText( + context.activity, + context.symphony.t.ExportedX(playlist.title), + Toast.LENGTH_SHORT, + ).show() + } catch (err: Exception) { + Logger.error("PlaylistTile", "export failed (activity result)", err) + Toast.makeText( + context.activity, + context.symphony.t.ExportFailedX( + err.localizedMessage ?: err.toString() + ), + Toast.LENGTH_SHORT, + ).show() + } } } } @@ -184,8 +185,18 @@ fun PlaylistDropdownMenu( Text(context.symphony.t.ShufflePlay) }, onClick = { + context.symphony.groove.coroutineScope.launch { + val songs = context.symphony.groove.playlist.findSongsById( + playlist.id, + context.symphony.settings.lastUsedPlaylistSongsSortBy.value, + context.symphony.settings.lastUsedPlaylistSongsSortReverse.value + ) + context.symphony.radio.clear() + context.symphony.radio.add(songs) + context.symphony.radio.setShuffleMode(true) + context.symphony.radio.play() + } onDismissRequest() - context.symphony.radio.shorty.playQueue(songs, shuffle = true) } ) DropdownMenuItem( @@ -196,11 +207,8 @@ fun PlaylistDropdownMenu( Text(context.symphony.t.PlayNext) }, onClick = { + // TODO onDismissRequest() - context.symphony.radio.queue.add( - songs, - context.symphony.radio.queue.currentSongIndex + 1 - ) } ) DropdownMenuItem( @@ -215,7 +223,7 @@ fun PlaylistDropdownMenu( showAddToPlaylistDialog = true } ) - if (!playlist.entity.isModifiable) { + if (!playlist.isModifiable) { DropdownMenuItem( leadingIcon = { Icon(Icons.AutoMirrored.Filled.PlaylistAdd, null) @@ -241,7 +249,7 @@ fun PlaylistDropdownMenu( showInfoDialog = true } ) - if (playlist.entity.isModifiable) { + if (playlist.isModifiable) { DropdownMenuItem( leadingIcon = { Icon(Icons.Filled.Save, null) @@ -252,7 +260,7 @@ fun PlaylistDropdownMenu( onClick = { onDismissRequest() try { - savePlaylistLauncher.launch("${playlist.entity.title}.m3u") + savePlaylistLauncher.launch("${playlist.title}.m3u") } catch (err: Exception) { Logger.error("PlaylistTile", "export failed", err) Toast.makeText( @@ -278,7 +286,7 @@ fun PlaylistDropdownMenu( } ) } - if (!playlist.entity.isInternal) { + if (!playlist.isInternal) { DropdownMenuItem( leadingIcon = { Icon( @@ -309,9 +317,18 @@ fun PlaylistDropdownMenu( } if (showSongsPicker) { + + val songs = remember { + context.symphony.groove.playlist.findSongsById( + playlist.id, + context.symphony.settings.lastUsedPlaylistSongsSortBy.value, + context.symphony.settings.lastUsedPlaylistSongsSortReverse.value + ) + } + PlaylistManageSongsDialog( context, - selectedSongIds = playlist.getSongIds(context.symphony), + selectedSongs = songs, onDone = { context.symphony.groove.playlist.update(playlist.id, it) showSongsPicker = false @@ -341,7 +358,7 @@ fun PlaylistDropdownMenu( if (showAddToPlaylistDialog) { AddToPlaylistDialog( context, - songIds = playlist.getSongIds(context.symphony), + songs = playlist.getSongIds(context.symphony), onDismissRequest = { showAddToPlaylistDialog = false } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/RenamePlaylistDialog.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/RenamePlaylistDialog.kt index cf26165b..14daefa0 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/RenamePlaylistDialog.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/RenamePlaylistDialog.kt @@ -18,14 +18,14 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import io.github.zyrouge.symphony.services.groove.Playlist +import io.github.zyrouge.symphony.services.groove.entities.Playlist import io.github.zyrouge.symphony.ui.helpers.ViewContext +import kotlinx.coroutines.launch @Composable fun RenamePlaylistDialog( context: ViewContext, playlist: Playlist, - onRename: () -> Unit = {}, onDismissRequest: () -> Unit, ) { var input by remember { mutableStateOf(playlist.title) } @@ -65,9 +65,10 @@ fun RenamePlaylistDialog( TextButton( enabled = input.isNotBlank() && input != playlist.title, onClick = { - onRename() + context.symphony.groove.coroutineScope.launch { + context.symphony.groove.playlist.save(playlist.copy(title = input)) + } onDismissRequest() - context.symphony.groove.playlist.renamePlaylist(playlist, input) } ) { Text(context.symphony.t.Done) diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongCard.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongCard.kt index 84b9b7aa..fba4c0e8 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongCard.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongCard.kt @@ -360,7 +360,7 @@ fun SongDropdownMenu( if (showAddToPlaylistDialog) { AddToPlaylistDialog( context, - songIds = listOf(song.id), + songs = listOf(song.id), onDismissRequest = { showAddToPlaylistDialog = false } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/SquareGrooveTile.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/SquareGrooveTile.kt index 7c5205b9..bc8b2e19 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/SquareGrooveTile.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/SquareGrooveTile.kt @@ -38,7 +38,7 @@ import coil.request.ImageRequest @OptIn(ExperimentalMaterial3Api::class) @Composable fun SquareGrooveTile( - image: ImageRequest, + images: List, options: @Composable (Boolean, () -> Unit) -> Unit, content: @Composable ColumnScope.() -> Unit, onPlay: () -> Unit, @@ -55,7 +55,8 @@ fun SquareGrooveTile( Column(horizontalAlignment = Alignment.CenterHorizontally) { Box { AsyncImage( - image, + // TODO + images.first(), null, contentScale = ContentScale.Crop, modifier = Modifier diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/helpers/UserInterface.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/helpers/UserInterface.kt index 051a105f..77161eef 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/helpers/UserInterface.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/helpers/UserInterface.kt @@ -2,9 +2,11 @@ package io.github.zyrouge.symphony.ui.helpers import android.content.Context import android.content.res.Configuration +import android.net.Uri import androidx.compose.foundation.layout.BoxWithConstraintsScope import androidx.compose.ui.unit.Dp import coil.request.ImageRequest +import io.github.zyrouge.symphony.Symphony enum class ScreenOrientation { PORTRAIT, @@ -29,10 +31,19 @@ enum class ScreenOrientation { } } -fun createHandyImageRequest(context: Context, image: Any, fallback: Int) = - createHandyImageRequest(context, image, fallbackResId = fallback) +fun createGrooveArtworkImageRequest(symphony: Symphony, uri: Uri?) = createHandyImageRequestBuilder( + symphony.applicationContext, + image = uri ?: Assets.getPlaceholderUri(symphony), + fallback = Assets.placeholderDarkId, +).build() -private fun createHandyImageRequest( +fun createGrooveArtworkImageRequests(symphony: Symphony, uris: List) = + uris.map { createGrooveArtworkImageRequest(symphony, it) } + +fun createHandyImageRequestBuilder(context: Context, image: Any, fallback: Int) = + createHandyImageRequestBuilder(context, image, fallbackResId = fallback) + +private fun createHandyImageRequestBuilder( context: Context, image: Any, fallbackResId: Int? = null, diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/PlaylistView.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/PlaylistView.kt index d40588a9..17ef5b56 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/PlaylistView.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/PlaylistView.kt @@ -23,6 +23,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.zyrouge.symphony.services.groove.repositories.PlaylistRepository import io.github.zyrouge.symphony.ui.components.AnimatedNowPlayingBottomBar import io.github.zyrouge.symphony.ui.components.IconTextBody diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/QueueView.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/QueueView.kt index fd2c38db..6d50798e 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/QueueView.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/QueueView.kt @@ -188,7 +188,7 @@ fun QueueView(context: ViewContext) { initialSongIds = queue.toList(), onDone = { playlist -> showSaveDialog = false - context.symphony.groove.playlist.add(playlist) + context.symphony.groove.playlist.addSongs(playlist) }, onDismissRequest = { showSaveDialog = false diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/ForYou.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/ForYou.kt index 605ecea5..f5e89259 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/ForYou.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/ForYou.kt @@ -34,9 +34,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -46,17 +44,17 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.compose.AsyncImage +import io.github.zyrouge.symphony.services.groove.entities.Album +import io.github.zyrouge.symphony.services.groove.entities.Artist import io.github.zyrouge.symphony.services.groove.repositories.SongRepository -import io.github.zyrouge.symphony.services.radio.Radio import io.github.zyrouge.symphony.ui.components.IconTextBody import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.ui.view.AlbumArtistViewRoute import io.github.zyrouge.symphony.ui.view.AlbumViewRoute import io.github.zyrouge.symphony.ui.view.ArtistViewRoute -import io.github.zyrouge.symphony.utils.builtin.randomSubList -import io.github.zyrouge.symphony.utils.builtin.runIfOrDefault import io.github.zyrouge.symphony.utils.builtin.subListNonStrict +import kotlinx.coroutines.launch enum class ForYou(val label: (context: ViewContext) -> String) { Albums(label = { it.symphony.t.SuggestedAlbums }), @@ -67,58 +65,36 @@ enum class ForYou(val label: (context: ViewContext) -> String) { @OptIn(ExperimentalMaterial3Api::class) @Composable fun ForYouView(context: ViewContext) { - val albumArtistsIsUpdating by context.symphony.groove.albumArtist.isUpdating.collectAsStateWithLifecycle() - val albumsIsUpdating by context.symphony.groove.album.isUpdating.collectAsStateWithLifecycle() - val artistsIsUpdating by context.symphony.groove.artist.isUpdating.collectAsStateWithLifecycle() - val songsIsUpdating by context.symphony.groove.song.isUpdating.collectAsStateWithLifecycle() - val albumArtistNames by context.symphony.groove.albumArtist.all.collectAsStateWithLifecycle() - val albumIds by context.symphony.groove.album.all.collectAsStateWithLifecycle() - val artistNames by context.symphony.groove.artist.all.collectAsStateWithLifecycle() - val songIds by context.symphony.groove.song.all.collectAsStateWithLifecycle() - val sortBy by context.symphony.settings.lastUsedSongsSortBy.flow.collectAsStateWithLifecycle() - val sortReverse by context.symphony.settings.lastUsedSongsSortReverse.flow.collectAsStateWithLifecycle() + val libraryIsUpdating by context.symphony.groove.exposer.isUpdating.collectAsStateWithLifecycle() + val recentlyAddedSongs by context.symphony.groove.song.valuesAsFlow( + SongRepository.SortBy.DATE_MODIFIED, + true, + 6 + ).collectAsStateWithLifecycle(emptyList()) when { - songIds.isNotEmpty() -> { - val sortedSongIds by remember(songsIsUpdating, songIds, sortBy, sortReverse) { - derivedStateOf { - runIfOrDefault(!songsIsUpdating, listOf()) { - context.symphony.groove.song.sort(songIds.toList(), sortBy, sortReverse) - } - } - } - val recentlyAddedSongs by remember(songsIsUpdating, songIds) { - derivedStateOf { - runIfOrDefault(!songsIsUpdating, listOf()) { - context.symphony.groove.song.sort( - songIds.toList(), - SongRepository.SortBy.DATE_MODIFIED, - true - ) - } - } - } - val randomAlbums by remember(albumsIsUpdating, albumIds) { - derivedStateOf { - runIfOrDefault(!albumsIsUpdating, listOf()) { - albumIds.randomSubList(6) - } - } - } - val randomArtists by remember(artistsIsUpdating, artistNames) { - derivedStateOf { - runIfOrDefault(!artistsIsUpdating, listOf()) { - artistNames.randomSubList(6) - } - } - } - val randomAlbumArtists by remember(albumArtistsIsUpdating, albumArtistNames) { - derivedStateOf { - runIfOrDefault(!albumArtistsIsUpdating, listOf()) { - albumArtistNames.randomSubList(6) - } - } - } + recentlyAddedSongs.isNotEmpty() -> { +// val randomAlbums by remember(albumsIsUpdating, albumIds) { +// derivedStateOf { +// runIfOrDefault(!albumsIsUpdating, listOf()) { +// albumIds.randomSubList(6) +// } +// } +// } +// val randomArtists by remember(artistsIsUpdating, artistNames) { +// derivedStateOf { +// runIfOrDefault(!artistsIsUpdating, listOf()) { +// artistNames.randomSubList(6) +// } +// } +// } +// val randomAlbumArtists by remember(albumArtistsIsUpdating, albumArtistNames) { +// derivedStateOf { +// runIfOrDefault(!albumArtistsIsUpdating, listOf()) { +// albumArtistNames.randomSubList(6) +// } +// } +// } Column(modifier = Modifier.verticalScroll(rememberScrollState())) { Row(modifier = Modifier.padding(20.dp, 0.dp)) { @@ -128,7 +104,7 @@ fun ForYouView(context: ViewContext) { text = { Text(context.symphony.t.PlayAll) }, - enabled = !songsIsUpdating, + enabled = !libraryIsUpdating, onClick = { context.symphony.radio.shorty.playQueue(sortedSongIds) }, @@ -141,7 +117,7 @@ fun ForYouView(context: ViewContext) { text = { Text(context.symphony.t.ShufflePlay) }, - enabled = !songsIsUpdating, + enabled = !libraryIsUpdating, onClick = { context.symphony.radio.shorty.playQueue( songIds.toList(), @@ -157,7 +133,6 @@ fun ForYouView(context: ViewContext) { } Spacer(modifier = Modifier.height(12.dp)) when { - songsIsUpdating -> SixGridLoading() recentlyAddedSongs.isEmpty() -> SixGridEmpty(context) else -> BoxWithConstraints { val tileWidth = this@BoxWithConstraints.maxWidth.times(0.7f) @@ -166,21 +141,20 @@ fun ForYouView(context: ViewContext) { horizontalArrangement = Arrangement.spacedBy(8.dp), ) { Spacer(modifier = Modifier.width(12.dp)) - recentlyAddedSongs.subListNonStrict(5).forEachIndexed { i, songId -> + recentlyAddedSongs.subListNonStrict(5).forEach { song -> val tileHeight = 96.dp val backgroundColor = MaterialTheme.colorScheme.surface - val song = context.symphony.groove.song.get(songId) - ?: return@forEachIndexed ElevatedCard( modifier = Modifier .width(tileWidth) .height(tileHeight), onClick = { - context.symphony.radio.shorty.playQueue( - recentlyAddedSongs, - options = Radio.PlayOptions(index = i), - ) + context.symphony.groove.coroutineScope.launch { + context.symphony.radio.clear() + context.symphony.radio.add(recentlyAddedSongs) + context.symphony.radio.playBySongId(song.id) + } } ) { Box { @@ -424,14 +398,8 @@ private fun SixGrid( private fun SuggestedAlbums( context: ViewContext, isLoading: Boolean, - albumIds: List, + albums: List, ) { - val albums by remember(albumIds) { - derivedStateOf { - context.symphony.groove.album.get(albumIds) - } - } - Spacer(modifier = Modifier.height(24.dp)) SideHeading { Text(context.symphony.t.SuggestedAlbums) @@ -444,7 +412,8 @@ private fun SuggestedAlbums( } ) { AsyncImage( - album.createArtworkImageRequest(context.symphony).build(), + album.createArtworkImageRequest(context.symphony.groove.album.getTop4ArtworkUriAsFlow()) + .build(), null, contentScale = ContentScale.Crop, modifier = Modifier @@ -462,14 +431,8 @@ private fun SuggestedArtists( context: ViewContext, label: String, isLoading: Boolean, - artistNames: List, + artists: List, ) { - val artists by remember(artistNames) { - derivedStateOf { - context.symphony.groove.artist.get(artistNames) - } - } - Spacer(modifier = Modifier.height(24.dp)) SideHeading { Text(label) @@ -500,27 +463,21 @@ private fun SuggestedAlbumArtists( context: ViewContext, label: String, isLoading: Boolean, - albumArtistNames: List, + artists: List, ) { - val albumArtists by remember(albumArtistNames) { - derivedStateOf { - context.symphony.groove.albumArtist.get(albumArtistNames) - } - } - Spacer(modifier = Modifier.height(24.dp)) SideHeading { Text(label) } Spacer(modifier = Modifier.height(12.dp)) - StatedSixGrid(context, isLoading, albumArtists) { albumArtist -> + StatedSixGrid(context, isLoading, artists) { artist -> Card( onClick = { - context.navController.navigate(AlbumArtistViewRoute(albumArtist.name)) + context.navController.navigate(ArtistViewRoute(artist.name)) } ) { AsyncImage( - albumArtist.createArtworkImageRequest(context.symphony).build(), + artist.createArtworkImageRequest(context.symphony).build(), null, contentScale = ContentScale.Crop, modifier = Modifier diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/HomePlaylistsView.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/HomePlaylistsView.kt index 22178019..556e7d6f 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/HomePlaylistsView.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/HomePlaylistsView.kt @@ -54,13 +54,16 @@ fun HomePlaylistsView(context: ViewContext) { uris.forEach { x -> try { ActivityHelper.makePersistableReadableUri(context.symphony.applicationContext, x) - val id = context.symphony.database.playlistsIdGenerator.next() - val parsed = Playlist.parse(context.symphony, id, x) + lateinit var parsed: Playlist.Parsed + val playlist = context.symphony.groove.playlist.create { id -> + parsed = Playlist.parse(context.symphony, id, x) + parsed.playlist + } val addOptions = PlaylistRepository.AddOptions( playlist = parsed.playlist, songPaths = parsed.songPaths, ) - context.symphony.groove.playlist.add(addOptions) + context.symphony.groove.playlist.addSongs(addOptions) } catch (err: Exception) { Logger.error("PlaylistView", "import failed (activity result)", err) Toast.makeText( @@ -98,7 +101,7 @@ fun HomePlaylistsView(context: ViewContext) { context, onDone = { addOptions -> showPlaylistCreator = false - context.symphony.groove.playlist.add(addOptions) + context.symphony.groove.playlist.addSongs(addOptions) }, onDismissRequest = { showPlaylistCreator = false diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsGrooveView.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsGrooveView.kt index e2a157d1..ab7974a4 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsGrooveView.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/SettingsGrooveView.kt @@ -329,8 +329,8 @@ fun ImagePreserver.Quality.label(context: ViewContext) = when (this) { } private fun refreshMediaLibrary(symphony: Symphony, clearCache: Boolean = false) { - symphony.radio.stop() symphony.groove.coroutineScope.launch { + symphony.radio.stop() val options = Groove.FetchOptions( resetInMemoryCache = true, resetPersistentCache = clearCache, diff --git a/i18n/en.toml b/i18n/en.toml index ce83b1f1..c8323c70 100644 --- a/i18n/en.toml +++ b/i18n/en.toml @@ -254,4 +254,5 @@ GaplessPlayback = "Gapless playback" GridColumns = "Grid columns" CaseSensitiveSorting = "Case sensitive sorting" KeepScreenAwakeOnLyrics = "Keep screen awake on lyrics content" -MinSongDurationFilter = "Minimum song duration filter" \ No newline at end of file +MinSongDurationFilter = "Minimum song duration filter" +ArtistCount = "Artist count"