diff --git a/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/5.json b/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/5.json new file mode 100644 index 0000000..e80f181 --- /dev/null +++ b/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/5.json @@ -0,0 +1,322 @@ +{ + "formatVersion": 1, + "database": { + "version": 5, + "identityHash": "c16206386ea59ba9109b1e116ec61ea0", + "entities": [ + { + "tableName": "Song", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `albumInfoId` INTEGER, `durationText` TEXT NOT NULL, `thumbnailUrl` TEXT, `lyrics` TEXT, `likedAt` INTEGER, `totalPlayTimeMs` INTEGER NOT NULL, `loudnessDb` REAL, `contentLength` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "albumInfoId", + "columnName": "albumInfoId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "durationText", + "columnName": "durationText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lyrics", + "columnName": "lyrics", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "likedAt", + "columnName": "likedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "totalPlayTimeMs", + "columnName": "totalPlayTimeMs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loudnessDb", + "columnName": "loudnessDb", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "contentLength", + "columnName": "contentLength", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SongInPlaylist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `playlistId` INTEGER NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `playlistId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`playlistId`) REFERENCES `Playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "playlistId", + "columnName": "playlistId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "songId", + "playlistId" + ] + }, + "indices": [ + { + "name": "index_SongInPlaylist_songId", + "unique": false, + "columnNames": [ + "songId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongInPlaylist_songId` ON `${TABLE_NAME}` (`songId`)" + }, + { + "name": "index_SongInPlaylist_playlistId", + "unique": false, + "columnNames": [ + "playlistId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongInPlaylist_playlistId` ON `${TABLE_NAME}` (`playlistId`)" + } + ], + "foreignKeys": [ + { + "table": "Song", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "songId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "Playlist", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "playlistId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "Playlist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Info", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `browseId` TEXT, `text` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "browseId", + "columnName": "browseId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SongWithAuthors", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `authorInfoId` INTEGER NOT NULL, PRIMARY KEY(`songId`, `authorInfoId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`authorInfoId`) REFERENCES `Info`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "authorInfoId", + "columnName": "authorInfoId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "songId", + "authorInfoId" + ] + }, + "indices": [ + { + "name": "index_SongWithAuthors_authorInfoId", + "unique": false, + "columnNames": [ + "authorInfoId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongWithAuthors_authorInfoId` ON `${TABLE_NAME}` (`authorInfoId`)" + } + ], + "foreignKeys": [ + { + "table": "Song", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "songId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "Info", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "authorInfoId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SearchQuery", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "query", + "columnName": "query", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_SearchQuery_query", + "unique": true, + "columnNames": [ + "query" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_SearchQuery_query` ON `${TABLE_NAME}` (`query`)" + } + ], + "foreignKeys": [] + } + ], + "views": [ + { + "viewName": "SortedSongInPlaylist", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM SongInPlaylist ORDER BY position" + } + ], + "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, 'c16206386ea59ba9109b1e116ec61ea0')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt index 68db7f2..e247b20 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt @@ -129,12 +129,13 @@ interface Database { views = [ SortedSongInPlaylist::class ], - version = 4, + version = 5, exportSchema = true, autoMigrations = [ AutoMigration(from = 1, to = 2), AutoMigration(from = 2, to = 3), AutoMigration(from = 3, to = 4, spec = DatabaseInitializer.From3To4Migration::class), + AutoMigration(from = 4, to = 5), ], ) abstract class DatabaseInitializer protected constructor() : RoomDatabase() { diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Song.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Song.kt index c9f5f8f..aca2e4f 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Song.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Song.kt @@ -13,6 +13,8 @@ data class Song( val lyrics: String? = null, val likedAt: Long? = null, val totalPlayTimeMs: Long = 0, + val loudnessDb: Float? = null, + val contentLength: Long? = null, ) { val formattedTotalPlayTime: String get() { diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/services/PlayerService.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/services/PlayerService.kt index a4d9594..b480ee5 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/services/PlayerService.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/services/PlayerService.kt @@ -44,6 +44,7 @@ import com.google.common.util.concurrent.ListenableFuture import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.MainActivity import it.vfsfitvnm.vimusic.R +import it.vfsfitvnm.vimusic.internal import it.vfsfitvnm.vimusic.utils.* import it.vfsfitvnm.youtubemusic.Outcome import kotlinx.coroutines.* @@ -100,6 +101,8 @@ class PlayerService : MediaSessionService(), MediaSession.Callback, MediaNotific private val coroutineScope = CoroutineScope(Dispatchers.IO) + Job() + private val songPendingLoudnessDb = mutableMapOf() + override fun onCreate() { super.onCreate() @@ -136,6 +139,18 @@ class PlayerService : MediaSessionService(), MediaSession.Callback, MediaNotific .build() player.addListener(this) + + coroutineScope.launch(Dispatchers.IO) { + while (true) { + delay(1000) + withContext(Dispatchers.Main) { + println("volume: ${player.volume}") + } + songPendingLoudnessDb.forEach { (key, value) -> + println(" $key = $value") + } + } + } } override fun onDestroy() { @@ -269,13 +284,14 @@ class PlayerService : MediaSessionService(), MediaSession.Callback, MediaNotific val mediaItem = eventTime.timeline.getWindow(eventTime.windowIndex, Timeline.Window()).mediaItem - coroutineScope.launch(Dispatchers.IO) { - Database.insert(mediaItem) + Database.internal.queryExecutor.execute { Database.incrementTotalPlayTimeMs(mediaItem.mediaId, playbackStats.totalPlayTimeMs) } } override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { + normalizeVolume() + radio?.let { radio -> if (player.mediaItemCount - player.currentMediaItemIndex <= 3) { coroutineScope.launch(Dispatchers.Main) { @@ -285,6 +301,18 @@ class PlayerService : MediaSessionService(), MediaSession.Callback, MediaNotific } } + private fun normalizeVolume() { + player.volume = player.currentMediaItem?.mediaId?.let { mediaId -> + songPendingLoudnessDb.getOrElse(mediaId) { + player.currentMediaItem?.mediaMetadata?.extras?.getFloat("loudnessDb") + } + ?.takeIf { it > 0 } + ?.let { loudnessDb -> + (1f - (0.01f + loudnessDb / 15)).coerceIn(0.1f, 1f) + } + } ?: 1f + } + override fun onAddMediaItems( mediaSession: MediaSession, controller: MediaSession.ControllerInfo, @@ -454,10 +482,33 @@ class PlayerService : MediaSessionService(), MediaSession.Callback, MediaNotific val url = runBlocking(Dispatchers.IO) { it.vfsfitvnm.youtubemusic.YouTube.player(videoId) }.flatMap { body -> + val loudnessDb = body.playerConfig?.audioConfig?.loudnessDb?.toFloat() + + songPendingLoudnessDb[videoId] = loudnessDb + + runBlocking(Dispatchers.Main) { + normalizeVolume() + } + when (val status = body.playabilityStatus.status) { "OK" -> body.streamingData?.adaptiveFormats?.findLast { format -> format.itag == 251 || format.itag == 140 - }?.url?.let { Outcome.Success(it) } ?: Outcome.Error.Unhandled( + }?.let { format -> + val mediaItem = runBlocking(Dispatchers.Main) { + player.currentMediaItem + } + + if (mediaItem?.mediaId == videoId) { + Database.internal.queryExecutor.execute { + Database.update(Database.insert(mediaItem).copy( + loudnessDb = loudnessDb, + contentLength = format.contentLength + )) + } + } + + Outcome.Success(format.url) + } ?: Outcome.Error.Unhandled( PlaybackException( "Couldn't find a playable audio format", null, diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/MediaItemMenu.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/MediaItemMenu.kt index 9250ae1..797d8d1 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/MediaItemMenu.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/MediaItemMenu.kt @@ -236,9 +236,7 @@ fun BaseMediaItemMenu( coroutineScope.launch(Dispatchers.IO) { val playlistId = Database.playlist(playlist.id)?.id ?: Database.insert(playlist) - if (Database.song(mediaItem.mediaId) == null) { - Database.insert(mediaItem) - } + Database.insert(mediaItem) Database.insert( SongInPlaylist( diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/IntentUriScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/IntentUriScreen.kt index 69ab0e8..bc3847e 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/IntentUriScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/IntentUriScreen.kt @@ -111,9 +111,7 @@ fun IntentUriScreen(uri: Uri) { items.valueOrNull ?.map(YouTube.Item.Song::asMediaItem) ?.forEachIndexed { index, mediaItem -> - if (Database.song(mediaItem.mediaId) == null) { - Database.insert(mediaItem) - } + Database.insert(mediaItem) Database.insert( SongInPlaylist( diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/PlaylistOrAlbumScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/PlaylistOrAlbumScreen.kt index 60089fb..a60fc72 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/PlaylistOrAlbumScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/PlaylistOrAlbumScreen.kt @@ -175,11 +175,7 @@ fun PlaylistOrAlbumScreen( song .toMediaItem(browseId, album) ?.let { mediaItem -> - if (Database.song(mediaItem.mediaId) == null) { - Database.insert( - mediaItem - ) - } + Database.insert(mediaItem) Database.insert( SongInPlaylist( diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerBottomSheet.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerBottomSheet.kt index 51aa730..c3d6030 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerBottomSheet.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerBottomSheet.kt @@ -17,7 +17,6 @@ import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.scale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp @@ -29,7 +28,6 @@ import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.models.Song import it.vfsfitvnm.vimusic.ui.components.BottomSheet import it.vfsfitvnm.vimusic.ui.components.BottomSheetState -import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder import it.vfsfitvnm.vimusic.ui.screens.rememberLyricsRoute import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette import it.vfsfitvnm.vimusic.ui.styling.LocalTypography @@ -198,13 +196,15 @@ fun PlayerBottomSheet( lyricsOutcome = nextOutcome.flatMap { it.lyrics?.text().toNotNull() - }.map { - it ?: "" - }.map { + }.map { lyrics -> + lyrics ?: "" + }.map { lyrics -> withContext(Dispatchers.IO) { - Database.update((song ?: Database.insert(player.mediaItem!!)).copy(lyrics = it)) + (song ?: player.mediaItem?.let(Database::insert))?.let { + Database.update(it.copy(lyrics = lyrics)) + } } - it + lyrics } } }, @@ -218,9 +218,11 @@ fun PlayerBottomSheet( }) } }, - onLyricsUpdate = { + onLyricsUpdate = { lyrics -> coroutineScope.launch(Dispatchers.IO) { - Database.update((song ?: Database.insert(player.mediaItem!!)).copy(lyrics = it)) + (song ?: player.mediaItem?.let(Database::insert))?.let { + Database.update(it.copy(lyrics = lyrics)) + } } } ) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerView.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerView.kt index a0f51be..5dc0b41 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerView.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerView.kt @@ -342,9 +342,9 @@ fun PlayerView( modifier = Modifier .clickable { coroutineScope.launch(Dispatchers.IO) { - Database.update( - (song ?: Database.insert(player.mediaItem!!)).toggleLike() - ) + (song ?: player.mediaItem?.let(Database::insert))?.let { + Database.update(it.toggleLike()) + } } } .padding(horizontal = 16.dp) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/utils.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/utils.kt index f9d7fef..5fda306 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/utils.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/utils.kt @@ -137,7 +137,8 @@ val SongWithInfo.asMediaItem: MediaItem "albumId" to album?.browseId, "artistNames" to authors?.map { it.text }, "artistIds" to authors?.map { it.browseId }, - "durationText" to song.durationText + "durationText" to song.durationText, + "loudnessDb" to song.loudnessDb ) ) .build()