From 8fb55120257d95f85ca9311cb21ed16f6a11b30d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20=C4=90=E1=BB=A9c=20Tu=E1=BA=A5n=20Minh?= Date: Thu, 14 Mar 2024 23:45:29 +0700 Subject: [PATCH] Implementing notification for new releases of followed artists --- app/build.gradle.kts | 7 + .../9.json | 964 ++++++ app/src/main/AndroidManifest.xml | 8 +- .../maxrave/simpmusic/SimpMusicApplication.kt | 15 +- .../maxrave/simpmusic/data/db/Converters.kt | 12 + .../maxrave/simpmusic/data/db/DatabaseDao.kt | 23 + .../simpmusic/data/db/LocalDataSource.kt | 335 +- .../simpmusic/data/db/MusicDatabase.kt | 10 +- .../entities/FollowedArtistSingleAndAlbum.kt | 14 + .../data/db/entities/NotificationEntity.kt | 17 + .../data/repository/MainRepository.kt | 3036 +++++++++-------- .../simpmusic/di/LocalServiceModule.kt | 98 +- .../com/maxrave/simpmusic/extension/AllExt.kt | 76 +- .../simpmusic/service/SimpleMediaService.kt | 18 +- .../test/notification/NotificationHandler.kt | 101 + .../service/test/notification/NotifyWork.kt | 141 + .../com/maxrave/simpmusic/ui/MainActivity.kt | 1 + .../simpmusic/viewModel/SharedViewModel.kt | 346 +- app/src/main/res/values/strings.xml | 2 + .../maxrave/kotlinytmusicscraper/test/main.kt | 2 - 20 files changed, 3418 insertions(+), 1808 deletions(-) create mode 100644 app/schemas/com.maxrave.simpmusic.data.db.MusicDatabase/9.json create mode 100644 app/src/main/java/com/maxrave/simpmusic/data/db/entities/FollowedArtistSingleAndAlbum.kt create mode 100644 app/src/main/java/com/maxrave/simpmusic/data/db/entities/NotificationEntity.kt create mode 100644 app/src/main/java/com/maxrave/simpmusic/service/test/notification/NotificationHandler.kt create mode 100644 app/src/main/java/com/maxrave/simpmusic/service/test/notification/NotifyWork.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e31a08cb..13a52c67 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -181,6 +181,11 @@ dependencies { implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0") implementation("androidx.core:core-ktx:1.12.0") implementation("androidx.appcompat:appcompat:1.6.1") + + val work_version = "2.9.0" + implementation("androidx.work:work-runtime-ktx:$work_version") + androidTestImplementation("androidx.work:work-testing:$work_version") + //material design3 implementation("com.google.android.material:material:1.11.0") //runtime @@ -246,6 +251,8 @@ dependencies { implementation("androidx.fragment:fragment-ktx:1.6.2") //Hilt implementation("com.google.dagger:hilt-android:2.50") + implementation("androidx.hilt:hilt-work:1.2.0") + ksp("androidx.hilt:hilt-compiler:1.2.0") ksp("com.google.dagger:hilt-compiler:2.50") ksp("org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.8.0") //DataStore diff --git a/app/schemas/com.maxrave.simpmusic.data.db.MusicDatabase/9.json b/app/schemas/com.maxrave.simpmusic.data.db.MusicDatabase/9.json new file mode 100644 index 00000000..432ebde9 --- /dev/null +++ b/app/schemas/com.maxrave.simpmusic.data.db.MusicDatabase/9.json @@ -0,0 +1,964 @@ +{ + "formatVersion": 1, + "database": { + "version": 9, + "identityHash": "7aeb51e95a61c88942636866ce779d9a", + "entities": [ + { + "tableName": "new_format", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `itag` INTEGER NOT NULL, `mimeType` TEXT, `codecs` TEXT, `bitrate` INTEGER, `sampleRate` INTEGER, `contentLength` INTEGER, `loudnessDb` REAL, `lengthSeconds` INTEGER, `playbackTrackingVideostatsPlaybackUrl` TEXT, `playbackTrackingAtrUrl` TEXT, `playbackTrackingVideostatsWatchtimeUrl` TEXT, `cpn` TEXT, PRIMARY KEY(`videoId`))", + "fields": [ + { + "fieldPath": "videoId", + "columnName": "videoId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "itag", + "columnName": "itag", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mimeType", + "columnName": "mimeType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "codecs", + "columnName": "codecs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bitrate", + "columnName": "bitrate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sampleRate", + "columnName": "sampleRate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentLength", + "columnName": "contentLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "loudnessDb", + "columnName": "loudnessDb", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "lengthSeconds", + "columnName": "lengthSeconds", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "playbackTrackingVideostatsPlaybackUrl", + "columnName": "playbackTrackingVideostatsPlaybackUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "playbackTrackingAtrUrl", + "columnName": "playbackTrackingAtrUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "playbackTrackingVideostatsWatchtimeUrl", + "columnName": "playbackTrackingVideostatsWatchtimeUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "cpn", + "columnName": "cpn", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "videoId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "song_info", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `author` TEXT, `authorId` TEXT, `authorThumbnail` TEXT, `description` TEXT, `subscribers` TEXT, `viewCount` INTEGER, `uploadDate` TEXT, `like` INTEGER, `dislike` INTEGER, PRIMARY KEY(`videoId`))", + "fields": [ + { + "fieldPath": "videoId", + "columnName": "videoId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "authorId", + "columnName": "authorId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "authorThumbnail", + "columnName": "authorThumbnail", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "subscribers", + "columnName": "subscribers", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "viewCount", + "columnName": "viewCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "uploadDate", + "columnName": "uploadDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "like", + "columnName": "like", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dislike", + "columnName": "dislike", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "videoId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "search_history", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`query` TEXT NOT NULL, PRIMARY KEY(`query`))", + "fields": [ + { + "fieldPath": "query", + "columnName": "query", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "query" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "song", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `albumId` TEXT, `albumName` TEXT, `artistId` TEXT, `artistName` TEXT, `duration` TEXT NOT NULL, `durationSeconds` INTEGER NOT NULL, `isAvailable` INTEGER NOT NULL, `isExplicit` INTEGER NOT NULL, `likeStatus` TEXT NOT NULL, `thumbnails` TEXT, `title` TEXT NOT NULL, `videoType` TEXT NOT NULL, `category` TEXT, `resultType` TEXT, `liked` INTEGER NOT NULL, `totalPlayTime` INTEGER NOT NULL, `downloadState` INTEGER NOT NULL, `inLibrary` INTEGER NOT NULL, PRIMARY KEY(`videoId`))", + "fields": [ + { + "fieldPath": "videoId", + "columnName": "videoId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "albumId", + "columnName": "albumId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "albumName", + "columnName": "albumName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artistId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistName", + "columnName": "artistName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "durationSeconds", + "columnName": "durationSeconds", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isAvailable", + "columnName": "isAvailable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isExplicit", + "columnName": "isExplicit", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "likeStatus", + "columnName": "likeStatus", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "thumbnails", + "columnName": "thumbnails", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "videoType", + "columnName": "videoType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "resultType", + "columnName": "resultType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "liked", + "columnName": "liked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalPlayTime", + "columnName": "totalPlayTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "downloadState", + "columnName": "downloadState", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inLibrary", + "columnName": "inLibrary", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "videoId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "artist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`channelId` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnails` TEXT, `followed` INTEGER NOT NULL, `inLibrary` INTEGER NOT NULL, PRIMARY KEY(`channelId`))", + "fields": [ + { + "fieldPath": "channelId", + "columnName": "channelId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "thumbnails", + "columnName": "thumbnails", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "followed", + "columnName": "followed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inLibrary", + "columnName": "inLibrary", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "channelId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "album", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`browseId` TEXT NOT NULL, `artistId` TEXT, `artistName` TEXT, `audioPlaylistId` TEXT NOT NULL, `description` TEXT NOT NULL, `duration` TEXT, `durationSeconds` INTEGER NOT NULL, `thumbnails` TEXT, `title` TEXT NOT NULL, `trackCount` INTEGER NOT NULL, `tracks` TEXT, `type` TEXT NOT NULL, `year` TEXT, `liked` INTEGER NOT NULL, `inLibrary` INTEGER NOT NULL, `downloadState` INTEGER NOT NULL, PRIMARY KEY(`browseId`))", + "fields": [ + { + "fieldPath": "browseId", + "columnName": "browseId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "artistId", + "columnName": "artistId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistName", + "columnName": "artistName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "audioPlaylistId", + "columnName": "audioPlaylistId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durationSeconds", + "columnName": "durationSeconds", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "thumbnails", + "columnName": "thumbnails", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "trackCount", + "columnName": "trackCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tracks", + "columnName": "tracks", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "liked", + "columnName": "liked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inLibrary", + "columnName": "inLibrary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "downloadState", + "columnName": "downloadState", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "browseId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "playlist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `author` TEXT, `description` TEXT NOT NULL, `duration` TEXT NOT NULL, `durationSeconds` INTEGER NOT NULL, `privacy` TEXT NOT NULL, `thumbnails` TEXT NOT NULL, `title` TEXT NOT NULL, `trackCount` INTEGER NOT NULL, `tracks` TEXT, `year` TEXT, `liked` INTEGER NOT NULL, `inLibrary` INTEGER NOT NULL, `downloadState` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "durationSeconds", + "columnName": "durationSeconds", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "privacy", + "columnName": "privacy", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "thumbnails", + "columnName": "thumbnails", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "trackCount", + "columnName": "trackCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tracks", + "columnName": "tracks", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "liked", + "columnName": "liked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inLibrary", + "columnName": "inLibrary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "downloadState", + "columnName": "downloadState", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "local_playlist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `thumbnail` TEXT, `inLibrary` INTEGER NOT NULL, `downloadState` INTEGER NOT NULL, `synced_with_youtube_playlist` INTEGER NOT NULL DEFAULT 0, `youtubePlaylistId` TEXT, `youtube_sync_state` INTEGER NOT NULL DEFAULT 0, `tracks` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "thumbnail", + "columnName": "thumbnail", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inLibrary", + "columnName": "inLibrary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "downloadState", + "columnName": "downloadState", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "syncedWithYouTubePlaylist", + "columnName": "synced_with_youtube_playlist", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "youtubePlaylistId", + "columnName": "youtubePlaylistId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "syncState", + "columnName": "youtube_sync_state", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "tracks", + "columnName": "tracks", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "lyrics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `error` INTEGER NOT NULL, `lines` TEXT, `syncType` TEXT, PRIMARY KEY(`videoId`))", + "fields": [ + { + "fieldPath": "videoId", + "columnName": "videoId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lines", + "columnName": "lines", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "syncType", + "columnName": "syncType", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "videoId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "queue", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`queueId` INTEGER NOT NULL, `listTrack` TEXT NOT NULL, PRIMARY KEY(`queueId`))", + "fields": [ + { + "fieldPath": "queueId", + "columnName": "queueId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "listTrack", + "columnName": "listTrack", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "queueId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "set_video_id", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `setVideoId` TEXT, PRIMARY KEY(`videoId`))", + "fields": [ + { + "fieldPath": "videoId", + "columnName": "videoId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "setVideoId", + "columnName": "setVideoId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "videoId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "pair_song_local_playlist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` INTEGER NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, `inPlaylist` INTEGER NOT NULL, FOREIGN KEY(`playlistId`) REFERENCES `local_playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`videoId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playlistId", + "columnName": "playlistId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inPlaylist", + "columnName": "inPlaylist", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_pair_song_local_playlist_playlistId", + "unique": false, + "columnNames": [ + "playlistId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_pair_song_local_playlist_playlistId` ON `${TABLE_NAME}` (`playlistId`)" + }, + { + "name": "index_pair_song_local_playlist_songId", + "unique": false, + "columnNames": [ + "songId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_pair_song_local_playlist_songId` ON `${TABLE_NAME}` (`songId`)" + } + ], + "foreignKeys": [ + { + "table": "local_playlist", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "playlistId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "song", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "songId" + ], + "referencedColumns": [ + "videoId" + ] + } + ] + }, + { + "tableName": "GoogleAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT NOT NULL, `cache` TEXT, `isUsed` INTEGER NOT NULL, PRIMARY KEY(`email`))", + "fields": [ + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cache", + "columnName": "cache", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isUsed", + "columnName": "isUsed", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "email" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "followed_artist_single_and_album", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`channelId` TEXT NOT NULL, `name` TEXT NOT NULL, `single` TEXT NOT NULL, `album` TEXT NOT NULL, PRIMARY KEY(`channelId`))", + "fields": [ + { + "fieldPath": "channelId", + "columnName": "channelId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "single", + "columnName": "single", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "channelId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "notification", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `channelId` TEXT NOT NULL, `thumbnail` TEXT, `name` TEXT NOT NULL, `single` TEXT NOT NULL, `album` TEXT NOT NULL, `time` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "channelId", + "columnName": "channelId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "thumbnail", + "columnName": "thumbnail", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "single", + "columnName": "single", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "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, '7aeb51e95a61c88942636866ce779d9a')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e5e63e62..d99f02c3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -157,7 +157,12 @@ android:name="androidx.startup.InitializationProvider" android:authorities="${applicationId}.androidx-startup" android:exported="false" - tools:node="merge"> + tools:node="merge"> + + diff --git a/app/src/main/java/com/maxrave/simpmusic/SimpMusicApplication.kt b/app/src/main/java/com/maxrave/simpmusic/SimpMusicApplication.kt index dfc8d600..7ed30762 100644 --- a/app/src/main/java/com/maxrave/simpmusic/SimpMusicApplication.kt +++ b/app/src/main/java/com/maxrave/simpmusic/SimpMusicApplication.kt @@ -3,14 +3,27 @@ package com.maxrave.simpmusic import android.app.Application import android.util.Log import androidx.appcompat.app.AppCompatDelegate +import androidx.hilt.work.HiltWorkerFactory import androidx.media3.common.util.UnstableApi +import androidx.work.Configuration import cat.ereza.customactivityoncrash.config.CaocConfig import com.maxrave.simpmusic.ui.MainActivity import dagger.hilt.android.HiltAndroidApp +import javax.inject.Inject @HiltAndroidApp -class SimpMusicApplication: Application(){ +class SimpMusicApplication : Application(), Configuration.Provider { + + @Inject + lateinit var workerFactory: HiltWorkerFactory + + override val workManagerConfiguration: Configuration + get() = Configuration.Builder() + .setWorkerFactory(workerFactory) + .setMinimumLoggingLevel(Log.WARN) + .build() + @UnstableApi override fun onCreate() { super.onCreate() diff --git a/app/src/main/java/com/maxrave/simpmusic/data/db/Converters.kt b/app/src/main/java/com/maxrave/simpmusic/data/db/Converters.kt index 801e5a2a..e8370f1e 100644 --- a/app/src/main/java/com/maxrave/simpmusic/data/db/Converters.kt +++ b/app/src/main/java/com/maxrave/simpmusic/data/db/Converters.kt @@ -81,4 +81,16 @@ class Converters { @TypeConverter fun dateToTimestamp(date: LocalDateTime?): Long? = date?.atZone(ZoneOffset.UTC)?.toInstant()?.toEpochMilli() + + @TypeConverter + fun fromListMapToString(list: List>): String { + val gson = Gson() + return gson.toJson(list) + } + + @TypeConverter + fun fromStringToListMap(value: String): List> { + val listType: Type = object : TypeToken>>() {}.type + return Gson().fromJson(value, listType) + } } \ No newline at end of file diff --git a/app/src/main/java/com/maxrave/simpmusic/data/db/DatabaseDao.kt b/app/src/main/java/com/maxrave/simpmusic/data/db/DatabaseDao.kt index 9996ff31..26649159 100644 --- a/app/src/main/java/com/maxrave/simpmusic/data/db/DatabaseDao.kt +++ b/app/src/main/java/com/maxrave/simpmusic/data/db/DatabaseDao.kt @@ -9,10 +9,12 @@ import androidx.room.Transaction import androidx.sqlite.db.SupportSQLiteQuery import com.maxrave.simpmusic.data.db.entities.AlbumEntity import com.maxrave.simpmusic.data.db.entities.ArtistEntity +import com.maxrave.simpmusic.data.db.entities.FollowedArtistSingleAndAlbum import com.maxrave.simpmusic.data.db.entities.GoogleAccountEntity import com.maxrave.simpmusic.data.db.entities.LocalPlaylistEntity import com.maxrave.simpmusic.data.db.entities.LyricsEntity import com.maxrave.simpmusic.data.db.entities.NewFormatEntity +import com.maxrave.simpmusic.data.db.entities.NotificationEntity import com.maxrave.simpmusic.data.db.entities.PairSongLocalPlaylist import com.maxrave.simpmusic.data.db.entities.PlaylistEntity import com.maxrave.simpmusic.data.db.entities.QueueEntity @@ -420,4 +422,25 @@ interface DatabaseDao { videoId: String, inLibrary: LocalDateTime, ) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertFollowedArtistSingleAndAlbum(followedArtistSingleAndAlbum: FollowedArtistSingleAndAlbum) + + @Query("SELECT * FROM followed_artist_single_and_album WHERE channelId = :channelId") + suspend fun getFollowedArtistSingleAndAlbum(channelId: String): FollowedArtistSingleAndAlbum? + + @Query("DELETE FROM followed_artist_single_and_album WHERE channelId = :channelId") + suspend fun deleteFollowedArtistSingleAndAlbum(channelId: String) + + @Query("SELECT * FROM followed_artist_single_and_album") + suspend fun getAllFollowedArtistSingleAndAlbum(): List? + + @Insert + suspend fun insertNotification(notificationEntity: NotificationEntity) + + @Query("SELECT * FROM notification") + suspend fun getAllNotification(): List + + @Query("DELETE FROM notification WHERE id = :id") + suspend fun deleteNotification(id: Long) } diff --git a/app/src/main/java/com/maxrave/simpmusic/data/db/LocalDataSource.kt b/app/src/main/java/com/maxrave/simpmusic/data/db/LocalDataSource.kt index ebf7ffab..c106de74 100644 --- a/app/src/main/java/com/maxrave/simpmusic/data/db/LocalDataSource.kt +++ b/app/src/main/java/com/maxrave/simpmusic/data/db/LocalDataSource.kt @@ -2,10 +2,12 @@ package com.maxrave.simpmusic.data.db import com.maxrave.simpmusic.data.db.entities.AlbumEntity import com.maxrave.simpmusic.data.db.entities.ArtistEntity +import com.maxrave.simpmusic.data.db.entities.FollowedArtistSingleAndAlbum import com.maxrave.simpmusic.data.db.entities.GoogleAccountEntity import com.maxrave.simpmusic.data.db.entities.LocalPlaylistEntity import com.maxrave.simpmusic.data.db.entities.LyricsEntity import com.maxrave.simpmusic.data.db.entities.NewFormatEntity +import com.maxrave.simpmusic.data.db.entities.NotificationEntity import com.maxrave.simpmusic.data.db.entities.PairSongLocalPlaylist import com.maxrave.simpmusic.data.db.entities.PlaylistEntity import com.maxrave.simpmusic.data.db.entities.QueueEntity @@ -17,232 +19,259 @@ import java.time.LocalDateTime import javax.inject.Inject class LocalDataSource - @Inject - constructor(private val databaseDao: DatabaseDao) { - suspend fun getAllRecentData() = databaseDao.getAllRecentData() +@Inject +constructor(private val databaseDao: DatabaseDao) { + suspend fun getAllRecentData() = databaseDao.getAllRecentData() - suspend fun getAllDownloadedPlaylist() = databaseDao.getAllDownloadedPlaylist() + suspend fun getAllDownloadedPlaylist() = databaseDao.getAllDownloadedPlaylist() - suspend fun getSearchHistory() = databaseDao.getSearchHistory() + suspend fun getSearchHistory() = databaseDao.getSearchHistory() - suspend fun deleteSearchHistory() = databaseDao.deleteSearchHistory() + suspend fun deleteSearchHistory() = databaseDao.deleteSearchHistory() - suspend fun insertSearchHistory(searchHistory: SearchHistory) = databaseDao.insertSearchHistory(searchHistory) + suspend fun insertSearchHistory(searchHistory: SearchHistory) = + databaseDao.insertSearchHistory(searchHistory) - suspend fun getAllSongs() = databaseDao.getAllSongs() + suspend fun getAllSongs() = databaseDao.getAllSongs() - suspend fun getRecentSongs( - limit: Int, - offset: Int, - ) = databaseDao.getRecentSongs(limit, offset) + suspend fun getRecentSongs( + limit: Int, + offset: Int, + ) = databaseDao.getRecentSongs(limit, offset) - suspend fun getSongByListVideoId(primaryKeyList: List) = databaseDao.getSongByListVideoId(primaryKeyList) + suspend fun getSongByListVideoId(primaryKeyList: List) = + databaseDao.getSongByListVideoId(primaryKeyList) - suspend fun getDownloadedSongs() = databaseDao.getDownloadedSongs() + suspend fun getDownloadedSongs() = databaseDao.getDownloadedSongs() - suspend fun getDownloadedSongsAsFlow() = databaseDao.getDownloadedSongsAsFlow() + suspend fun getDownloadedSongsAsFlow() = databaseDao.getDownloadedSongsAsFlow() - suspend fun getDownloadingSongs() = databaseDao.getDownloadingSongs() + suspend fun getDownloadingSongs() = databaseDao.getDownloadingSongs() - suspend fun getLikedSongs() = databaseDao.getLikedSongs() + suspend fun getLikedSongs() = databaseDao.getLikedSongs() - suspend fun getLibrarySongs() = databaseDao.getLibrarySongs() + suspend fun getLibrarySongs() = databaseDao.getLibrarySongs() - suspend fun getSong(videoId: String) = databaseDao.getSong(videoId) + suspend fun getSong(videoId: String) = databaseDao.getSong(videoId) - suspend fun insertSong(song: SongEntity) = databaseDao.insertSong(song) + suspend fun insertSong(song: SongEntity) = databaseDao.insertSong(song) - suspend fun updateListenCount(videoId: String) = databaseDao.updateTotalPlayTime(videoId) + suspend fun updateListenCount(videoId: String) = databaseDao.updateTotalPlayTime(videoId) - suspend fun updateLiked( - liked: Int, - videoId: String, - ) = databaseDao.updateLiked(liked, videoId) + suspend fun updateLiked( + liked: Int, + videoId: String, + ) = databaseDao.updateLiked(liked, videoId) - suspend fun updateDurationSeconds( - durationSeconds: Int, - videoId: String, - ) = databaseDao.updateDurationSeconds(durationSeconds, videoId) + suspend fun updateDurationSeconds( + durationSeconds: Int, + videoId: String, + ) = databaseDao.updateDurationSeconds(durationSeconds, videoId) - suspend fun updateSongInLibrary( - inLibrary: LocalDateTime, - videoId: String, - ) = databaseDao.updateSongInLibrary(inLibrary, videoId) + suspend fun updateSongInLibrary( + inLibrary: LocalDateTime, + videoId: String, + ) = databaseDao.updateSongInLibrary(inLibrary, videoId) - suspend fun getMostPlayedSongs() = databaseDao.getMostPlayedSongs() + suspend fun getMostPlayedSongs() = databaseDao.getMostPlayedSongs() - suspend fun updateDownloadState( - downloadState: Int, - videoId: String, - ) = databaseDao.updateDownloadState(downloadState, videoId) + suspend fun updateDownloadState( + downloadState: Int, + videoId: String, + ) = databaseDao.updateDownloadState(downloadState, videoId) - suspend fun getAllArtists() = databaseDao.getAllArtists() + suspend fun getAllArtists() = databaseDao.getAllArtists() - suspend fun insertArtist(artist: ArtistEntity) = databaseDao.insertArtist(artist) + suspend fun insertArtist(artist: ArtistEntity) = databaseDao.insertArtist(artist) - suspend fun updateFollowed( - followed: Int, - channelId: String, - ) = databaseDao.updateFollowed(followed, channelId) + suspend fun updateFollowed( + followed: Int, + channelId: String, + ) = databaseDao.updateFollowed(followed, channelId) - suspend fun getArtist(channelId: String) = databaseDao.getArtist(channelId) + suspend fun getArtist(channelId: String) = databaseDao.getArtist(channelId) - suspend fun getFollowedArtists() = databaseDao.getFollowedArtists() + suspend fun getFollowedArtists() = databaseDao.getFollowedArtists() - suspend fun updateArtistInLibrary( - inLibrary: LocalDateTime, - channelId: String, - ) = databaseDao.updateArtistInLibrary(inLibrary, channelId) + suspend fun updateArtistInLibrary( + inLibrary: LocalDateTime, + channelId: String, + ) = databaseDao.updateArtistInLibrary(inLibrary, channelId) - suspend fun getAllAlbums() = databaseDao.getAllAlbums() + suspend fun getAllAlbums() = databaseDao.getAllAlbums() - suspend fun insertAlbum(album: AlbumEntity) = databaseDao.insertAlbum(album) + suspend fun insertAlbum(album: AlbumEntity) = databaseDao.insertAlbum(album) - suspend fun updateAlbumLiked( - liked: Int, - albumId: String, - ) = databaseDao.updateAlbumLiked(liked, albumId) + suspend fun updateAlbumLiked( + liked: Int, + albumId: String, + ) = databaseDao.updateAlbumLiked(liked, albumId) - suspend fun getAlbum(albumId: String) = databaseDao.getAlbum(albumId) + suspend fun getAlbum(albumId: String) = databaseDao.getAlbum(albumId) - suspend fun getLikedAlbums() = databaseDao.getLikedAlbums() + suspend fun getLikedAlbums() = databaseDao.getLikedAlbums() - suspend fun updateAlbumInLibrary( - inLibrary: LocalDateTime, - albumId: String, - ) = databaseDao.updateAlbumInLibrary(inLibrary, albumId) + suspend fun updateAlbumInLibrary( + inLibrary: LocalDateTime, + albumId: String, + ) = databaseDao.updateAlbumInLibrary(inLibrary, albumId) - suspend fun updateAlbumDownloadState( - downloadState: Int, - albumId: String, - ) = databaseDao.updateAlbumDownloadState(downloadState, albumId) + suspend fun updateAlbumDownloadState( + downloadState: Int, + albumId: String, + ) = databaseDao.updateAlbumDownloadState(downloadState, albumId) - suspend fun getAllPlaylists() = databaseDao.getAllPlaylists() + suspend fun getAllPlaylists() = databaseDao.getAllPlaylists() - suspend fun insertPlaylist(playlist: PlaylistEntity) = databaseDao.insertPlaylist(playlist) + suspend fun insertPlaylist(playlist: PlaylistEntity) = databaseDao.insertPlaylist(playlist) - suspend fun insertRadioPlaylist(playlist: PlaylistEntity) = databaseDao.insertRadioPlaylist(playlist) + suspend fun insertRadioPlaylist(playlist: PlaylistEntity) = + databaseDao.insertRadioPlaylist(playlist) - suspend fun updatePlaylistLiked( - liked: Int, - playlistId: String, - ) = databaseDao.updatePlaylistLiked(liked, playlistId) + suspend fun updatePlaylistLiked( + liked: Int, + playlistId: String, + ) = databaseDao.updatePlaylistLiked(liked, playlistId) - suspend fun getPlaylist(playlistId: String) = databaseDao.getPlaylist(playlistId) + suspend fun getPlaylist(playlistId: String) = databaseDao.getPlaylist(playlistId) - suspend fun getLikedPlaylists() = databaseDao.getLikedPlaylists() + suspend fun getLikedPlaylists() = databaseDao.getLikedPlaylists() - suspend fun updatePlaylistInLibrary( - inLibrary: LocalDateTime, - playlistId: String, - ) = databaseDao.updatePlaylistInLibrary(inLibrary, playlistId) + suspend fun updatePlaylistInLibrary( + inLibrary: LocalDateTime, + playlistId: String, + ) = databaseDao.updatePlaylistInLibrary(inLibrary, playlistId) - suspend fun updatePlaylistDownloadState( - downloadState: Int, - playlistId: String, - ) = databaseDao.updatePlaylistDownloadState(downloadState, playlistId) + suspend fun updatePlaylistDownloadState( + downloadState: Int, + playlistId: String, + ) = databaseDao.updatePlaylistDownloadState(downloadState, playlistId) - suspend fun getAllLocalPlaylists() = databaseDao.getAllLocalPlaylists() + suspend fun getAllLocalPlaylists() = databaseDao.getAllLocalPlaylists() - suspend fun getLocalPlaylist(id: Long) = databaseDao.getLocalPlaylist(id) + suspend fun getLocalPlaylist(id: Long) = databaseDao.getLocalPlaylist(id) - suspend fun insertLocalPlaylist(localPlaylist: LocalPlaylistEntity) = databaseDao.insertLocalPlaylist(localPlaylist) + suspend fun insertLocalPlaylist(localPlaylist: LocalPlaylistEntity) = + databaseDao.insertLocalPlaylist(localPlaylist) - suspend fun deleteLocalPlaylist(id: Long) = databaseDao.deleteLocalPlaylist(id) + suspend fun deleteLocalPlaylist(id: Long) = databaseDao.deleteLocalPlaylist(id) - suspend fun updateLocalPlaylistTitle( - title: String, - id: Long, - ) = databaseDao.updateLocalPlaylistTitle(title, id) + suspend fun updateLocalPlaylistTitle( + title: String, + id: Long, + ) = databaseDao.updateLocalPlaylistTitle(title, id) - suspend fun updateLocalPlaylistThumbnail( - thumbnail: String, - id: Long, - ) = databaseDao.updateLocalPlaylistThumbnail(thumbnail, id) + suspend fun updateLocalPlaylistThumbnail( + thumbnail: String, + id: Long, + ) = databaseDao.updateLocalPlaylistThumbnail(thumbnail, id) - suspend fun updateLocalPlaylistTracks( - tracks: List, - id: Long, - ) = databaseDao.updateLocalPlaylistTracks(tracks, id) + suspend fun updateLocalPlaylistTracks( + tracks: List, + id: Long, + ) = databaseDao.updateLocalPlaylistTracks(tracks, id) - suspend fun updateLocalPlaylistInLibrary( - inLibrary: LocalDateTime, - id: Long, - ) = databaseDao.updateLocalPlaylistInLibrary(inLibrary, id) + suspend fun updateLocalPlaylistInLibrary( + inLibrary: LocalDateTime, + id: Long, + ) = databaseDao.updateLocalPlaylistInLibrary(inLibrary, id) - suspend fun updateLocalPlaylistDownloadState( - downloadState: Int, - id: Long, - ) = databaseDao.updateLocalPlaylistDownloadState(downloadState, id) + suspend fun updateLocalPlaylistDownloadState( + downloadState: Int, + id: Long, + ) = databaseDao.updateLocalPlaylistDownloadState(downloadState, id) - suspend fun getDownloadedLocalPlaylists() = databaseDao.getDownloadedLocalPlaylists() + suspend fun getDownloadedLocalPlaylists() = databaseDao.getDownloadedLocalPlaylists() - suspend fun updateLocalPlaylistYouTubePlaylistId( - id: Long, - ytId: String?, - ) = databaseDao.updateLocalPlaylistYouTubePlaylistId(id, ytId) + suspend fun updateLocalPlaylistYouTubePlaylistId( + id: Long, + ytId: String?, + ) = databaseDao.updateLocalPlaylistYouTubePlaylistId(id, ytId) - suspend fun updateLocalPlaylistYouTubePlaylistSynced( - id: Long, - synced: Int, - ) = databaseDao.updateLocalPlaylistYouTubePlaylistSynced(id, synced) + suspend fun updateLocalPlaylistYouTubePlaylistSynced( + id: Long, + synced: Int, + ) = databaseDao.updateLocalPlaylistYouTubePlaylistSynced(id, synced) - suspend fun updateLocalPlaylistYouTubePlaylistSyncState( - id: Long, - syncState: Int, - ) = databaseDao.updateLocalPlaylistYouTubePlaylistSyncState(id, syncState) + suspend fun updateLocalPlaylistYouTubePlaylistSyncState( + id: Long, + syncState: Int, + ) = databaseDao.updateLocalPlaylistYouTubePlaylistSyncState(id, syncState) - suspend fun getSavedLyrics(videoId: String) = databaseDao.getLyrics(videoId) + suspend fun getSavedLyrics(videoId: String) = databaseDao.getLyrics(videoId) - suspend fun insertLyrics(lyrics: LyricsEntity) = databaseDao.insertLyrics(lyrics) + suspend fun insertLyrics(lyrics: LyricsEntity) = databaseDao.insertLyrics(lyrics) - suspend fun getPreparingSongs() = databaseDao.getPreparingSongs() + suspend fun getPreparingSongs() = databaseDao.getPreparingSongs() - suspend fun insertNewFormat(format: NewFormatEntity) = databaseDao.insertNewFormat(format) + suspend fun insertNewFormat(format: NewFormatEntity) = databaseDao.insertNewFormat(format) - suspend fun getNewFormat(videoId: String) = databaseDao.getNewFormat(videoId) + suspend fun getNewFormat(videoId: String) = databaseDao.getNewFormat(videoId) - suspend fun insertSongInfo(songInfo: SongInfoEntity) = databaseDao.insertSongInfo(songInfo) + suspend fun insertSongInfo(songInfo: SongInfoEntity) = databaseDao.insertSongInfo(songInfo) - suspend fun getSongInfo(videoId: String) = databaseDao.getSongInfo(videoId) + suspend fun getSongInfo(videoId: String) = databaseDao.getSongInfo(videoId) - suspend fun recoverQueue(queueEntity: QueueEntity) = databaseDao.recoverQueue(queueEntity) + suspend fun recoverQueue(queueEntity: QueueEntity) = databaseDao.recoverQueue(queueEntity) - suspend fun getQueue() = databaseDao.getQueue() + suspend fun getQueue() = databaseDao.getQueue() - suspend fun deleteQueue() = databaseDao.deleteQueue() + suspend fun deleteQueue() = databaseDao.deleteQueue() - suspend fun getLocalPlaylistByYoutubePlaylistId(playlistId: String) = databaseDao.getLocalPlaylistByYoutubePlaylistId(playlistId) + suspend fun getLocalPlaylistByYoutubePlaylistId(playlistId: String) = + databaseDao.getLocalPlaylistByYoutubePlaylistId(playlistId) - suspend fun insertSetVideoId(setVideoIdEntity: SetVideoIdEntity) = databaseDao.insertSetVideoId(setVideoIdEntity) + suspend fun insertSetVideoId(setVideoIdEntity: SetVideoIdEntity) = + databaseDao.insertSetVideoId(setVideoIdEntity) - suspend fun getSetVideoId(videoId: String) = databaseDao.getSetVideoId(videoId) + suspend fun getSetVideoId(videoId: String) = databaseDao.getSetVideoId(videoId) - suspend fun insertPairSongLocalPlaylist(pairSongLocalPlaylist: PairSongLocalPlaylist) = - databaseDao.insertPairSongLocalPlaylist(pairSongLocalPlaylist) + suspend fun insertPairSongLocalPlaylist(pairSongLocalPlaylist: PairSongLocalPlaylist) = + databaseDao.insertPairSongLocalPlaylist(pairSongLocalPlaylist) - suspend fun getPlaylistPairSong(playlistId: Long) = databaseDao.getPlaylistPairSong(playlistId) + suspend fun getPlaylistPairSong(playlistId: Long) = databaseDao.getPlaylistPairSong(playlistId) - suspend fun deletePairSongLocalPlaylist( - playlistId: Long, - videoId: String, - ) = databaseDao.deletePairSongLocalPlaylist(playlistId, videoId) + suspend fun deletePairSongLocalPlaylist( + playlistId: Long, + videoId: String, + ) = databaseDao.deletePairSongLocalPlaylist(playlistId, videoId) - suspend fun getGoogleAccounts() = databaseDao.getAllGoogleAccount() + suspend fun getGoogleAccounts() = databaseDao.getAllGoogleAccount() - suspend fun insertGoogleAccount(googleAccountEntity: GoogleAccountEntity) = databaseDao.insertGoogleAccount(googleAccountEntity) + suspend fun insertGoogleAccount(googleAccountEntity: GoogleAccountEntity) = + databaseDao.insertGoogleAccount(googleAccountEntity) - suspend fun getUsedGoogleAccount() = databaseDao.getUsedGoogleAccount() + suspend fun getUsedGoogleAccount() = databaseDao.getUsedGoogleAccount() - suspend fun deleteGoogleAccount(email: String) = databaseDao.deleteGoogleAccount(email) + suspend fun deleteGoogleAccount(email: String) = databaseDao.deleteGoogleAccount(email) - suspend fun updateGoogleAccountUsed( - email: String, - isUsed: Boolean, - ) = databaseDao.updateGoogleAccountUsed(isUsed, email) + suspend fun updateGoogleAccountUsed( + email: String, + isUsed: Boolean, + ) = databaseDao.updateGoogleAccountUsed(isUsed, email) - suspend fun setInLibrary( - videoId: String, - inLibrary: LocalDateTime, - ) = databaseDao.setInLibrary(videoId, inLibrary) - } + suspend fun setInLibrary( + videoId: String, + inLibrary: LocalDateTime, + ) = databaseDao.setInLibrary(videoId, inLibrary) + + suspend fun insertFollowedArtistSingleAndAlbum(followedArtistSingleAndAlbum: FollowedArtistSingleAndAlbum) = + databaseDao.insertFollowedArtistSingleAndAlbum(followedArtistSingleAndAlbum) + + suspend fun deleteFollowedArtistSingleAndAlbum(channelId: String) = + databaseDao.deleteFollowedArtistSingleAndAlbum(channelId) + + suspend fun getFollowedArtistSingleAndAlbum(channelId: String) = + databaseDao.getFollowedArtistSingleAndAlbum(channelId) + + suspend fun getAllFollowedArtistSingleAndAlbums() = + databaseDao.getAllFollowedArtistSingleAndAlbum() + + suspend fun insertNotification(notificationEntity: NotificationEntity) = + databaseDao.insertNotification(notificationEntity) + + suspend fun getAllNotification() = databaseDao.getAllNotification() + + suspend fun deleteNotification(id: Long) = databaseDao.deleteNotification(id) + +} diff --git a/app/src/main/java/com/maxrave/simpmusic/data/db/MusicDatabase.kt b/app/src/main/java/com/maxrave/simpmusic/data/db/MusicDatabase.kt index 079f93de..5857c0fb 100644 --- a/app/src/main/java/com/maxrave/simpmusic/data/db/MusicDatabase.kt +++ b/app/src/main/java/com/maxrave/simpmusic/data/db/MusicDatabase.kt @@ -6,10 +6,12 @@ import androidx.room.RoomDatabase import androidx.room.TypeConverters import com.maxrave.simpmusic.data.db.entities.AlbumEntity import com.maxrave.simpmusic.data.db.entities.ArtistEntity +import com.maxrave.simpmusic.data.db.entities.FollowedArtistSingleAndAlbum import com.maxrave.simpmusic.data.db.entities.GoogleAccountEntity import com.maxrave.simpmusic.data.db.entities.LocalPlaylistEntity import com.maxrave.simpmusic.data.db.entities.LyricsEntity import com.maxrave.simpmusic.data.db.entities.NewFormatEntity +import com.maxrave.simpmusic.data.db.entities.NotificationEntity import com.maxrave.simpmusic.data.db.entities.PairSongLocalPlaylist import com.maxrave.simpmusic.data.db.entities.PlaylistEntity import com.maxrave.simpmusic.data.db.entities.QueueEntity @@ -19,8 +21,8 @@ import com.maxrave.simpmusic.data.db.entities.SongEntity import com.maxrave.simpmusic.data.db.entities.SongInfoEntity @Database( - entities = [NewFormatEntity::class, SongInfoEntity::class, SearchHistory::class, SongEntity::class, ArtistEntity::class, AlbumEntity::class, PlaylistEntity::class, LocalPlaylistEntity::class, LyricsEntity::class, QueueEntity::class, SetVideoIdEntity::class, PairSongLocalPlaylist::class, GoogleAccountEntity::class], - version = 8, + entities = [NewFormatEntity::class, SongInfoEntity::class, SearchHistory::class, SongEntity::class, ArtistEntity::class, AlbumEntity::class, PlaylistEntity::class, LocalPlaylistEntity::class, LyricsEntity::class, QueueEntity::class, SetVideoIdEntity::class, PairSongLocalPlaylist::class, GoogleAccountEntity::class, FollowedArtistSingleAndAlbum::class, NotificationEntity::class], + version = 9, exportSchema = true, autoMigrations = [AutoMigration(from = 2, to = 3), AutoMigration( from = 1, @@ -32,9 +34,9 @@ import com.maxrave.simpmusic.data.db.entities.SongInfoEntity 7, 8, spec = AutoMigration7_8::class - )] + ), AutoMigration(8, 9)] ) @TypeConverters(Converters::class) -abstract class MusicDatabase: RoomDatabase() { +abstract class MusicDatabase : RoomDatabase() { abstract fun getDatabaseDao(): DatabaseDao } \ No newline at end of file diff --git a/app/src/main/java/com/maxrave/simpmusic/data/db/entities/FollowedArtistSingleAndAlbum.kt b/app/src/main/java/com/maxrave/simpmusic/data/db/entities/FollowedArtistSingleAndAlbum.kt new file mode 100644 index 00000000..b86e0e3a --- /dev/null +++ b/app/src/main/java/com/maxrave/simpmusic/data/db/entities/FollowedArtistSingleAndAlbum.kt @@ -0,0 +1,14 @@ +package com.maxrave.simpmusic.data.db.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "followed_artist_single_and_album") +data class FollowedArtistSingleAndAlbum( + @PrimaryKey(autoGenerate = false) val channelId: String, + val name: String, + val single: List> = listOf(), + val album: List> = listOf() + +) { +} \ No newline at end of file diff --git a/app/src/main/java/com/maxrave/simpmusic/data/db/entities/NotificationEntity.kt b/app/src/main/java/com/maxrave/simpmusic/data/db/entities/NotificationEntity.kt new file mode 100644 index 00000000..af84f405 --- /dev/null +++ b/app/src/main/java/com/maxrave/simpmusic/data/db/entities/NotificationEntity.kt @@ -0,0 +1,17 @@ +package com.maxrave.simpmusic.data.db.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey +import java.time.LocalDateTime + +@Entity(tableName = "notification") +data class NotificationEntity( + @PrimaryKey(autoGenerate = true) val id: Long = 0L, + val channelId: String, + val thumbnail: String? = null, + val name: String, + val single: List> = listOf(), + val album: List> = listOf(), + val time: LocalDateTime = LocalDateTime.now() +) { +} \ No newline at end of file diff --git a/app/src/main/java/com/maxrave/simpmusic/data/repository/MainRepository.kt b/app/src/main/java/com/maxrave/simpmusic/data/repository/MainRepository.kt index aad335af..54c6f064 100644 --- a/app/src/main/java/com/maxrave/simpmusic/data/repository/MainRepository.kt +++ b/app/src/main/java/com/maxrave/simpmusic/data/repository/MainRepository.kt @@ -26,6 +26,7 @@ import com.maxrave.simpmusic.data.dataStore.DataStoreManager import com.maxrave.simpmusic.data.db.LocalDataSource import com.maxrave.simpmusic.data.db.entities.AlbumEntity import com.maxrave.simpmusic.data.db.entities.ArtistEntity +import com.maxrave.simpmusic.data.db.entities.FollowedArtistSingleAndAlbum import com.maxrave.simpmusic.data.db.entities.GoogleAccountEntity import com.maxrave.simpmusic.data.db.entities.LocalPlaylistEntity import com.maxrave.simpmusic.data.db.entities.LyricsEntity @@ -101,620 +102,671 @@ import kotlin.math.abs // @ActivityRetainedScoped @Singleton class MainRepository - @Inject - constructor( - private val localDataSource: LocalDataSource, - private val dataStoreManager: DataStoreManager, - @ApplicationContext private val context: Context, - ) { - // Database - suspend fun getSearchHistory(): Flow> = flow { emit(localDataSource.getSearchHistory()) }.flowOn(Dispatchers.IO) +@Inject +constructor( + private val localDataSource: LocalDataSource, + private val dataStoreManager: DataStoreManager, + @ApplicationContext private val context: Context, +) { + // Database + suspend fun getSearchHistory(): Flow> = + flow { emit(localDataSource.getSearchHistory()) }.flowOn(Dispatchers.IO) - suspend fun insertSearchHistory(searchHistory: SearchHistory) = - withContext(Dispatchers.IO) { localDataSource.insertSearchHistory(searchHistory) } + suspend fun insertSearchHistory(searchHistory: SearchHistory) = + withContext(Dispatchers.IO) { localDataSource.insertSearchHistory(searchHistory) } - suspend fun deleteSearchHistory() = withContext(Dispatchers.IO) { localDataSource.deleteSearchHistory() } + suspend fun deleteSearchHistory() = + withContext(Dispatchers.IO) { localDataSource.deleteSearchHistory() } - suspend fun getAllSongs(): Flow> = flow { emit(localDataSource.getAllSongs()) }.flowOn(Dispatchers.IO) + suspend fun getAllSongs(): Flow> = + flow { emit(localDataSource.getAllSongs()) }.flowOn(Dispatchers.IO) - suspend fun setInLibrary( - videoId: String, - inLibrary: LocalDateTime, - ) = withContext(Dispatchers.IO) { localDataSource.setInLibrary(videoId, inLibrary) } + suspend fun setInLibrary( + videoId: String, + inLibrary: LocalDateTime, + ) = withContext(Dispatchers.IO) { localDataSource.setInLibrary(videoId, inLibrary) } - suspend fun getSongsByListVideoId(listVideoId: List): Flow> = - flow { emit(localDataSource.getSongByListVideoId(listVideoId)) }.flowOn(Dispatchers.IO) + suspend fun getSongsByListVideoId(listVideoId: List): Flow> = + flow { emit(localDataSource.getSongByListVideoId(listVideoId)) }.flowOn(Dispatchers.IO) - suspend fun getDownloadedSongs(): Flow?> = - flow { emit(localDataSource.getDownloadedSongs()) }.flowOn(Dispatchers.IO) + suspend fun getDownloadedSongs(): Flow?> = + flow { emit(localDataSource.getDownloadedSongs()) }.flowOn(Dispatchers.IO) - suspend fun getDownloadedSongsAsFlow() = localDataSource.getDownloadedSongsAsFlow() + suspend fun getDownloadedSongsAsFlow() = localDataSource.getDownloadedSongsAsFlow() - suspend fun getDownloadingSongs(): Flow?> = - flow { emit(localDataSource.getDownloadingSongs()) }.flowOn(Dispatchers.IO) + suspend fun getDownloadingSongs(): Flow?> = + flow { emit(localDataSource.getDownloadingSongs()) }.flowOn(Dispatchers.IO) - suspend fun getPreparingSongs(): Flow> = flow { emit(localDataSource.getPreparingSongs()) }.flowOn(Dispatchers.IO) + suspend fun getPreparingSongs(): Flow> = + flow { emit(localDataSource.getPreparingSongs()) }.flowOn(Dispatchers.IO) - suspend fun getLikedSongs(): Flow> = flow { emit(localDataSource.getLikedSongs()) }.flowOn(Dispatchers.IO) + suspend fun getLikedSongs(): Flow> = + flow { emit(localDataSource.getLikedSongs()) }.flowOn(Dispatchers.IO) - suspend fun getLibrarySongs(): Flow> = flow { emit(localDataSource.getLibrarySongs()) }.flowOn(Dispatchers.IO) + suspend fun getLibrarySongs(): Flow> = + flow { emit(localDataSource.getLibrarySongs()) }.flowOn(Dispatchers.IO) - suspend fun getSongById(id: String): Flow = flow { emit(localDataSource.getSong(id)) }.flowOn(Dispatchers.IO) + suspend fun getSongById(id: String): Flow = + flow { emit(localDataSource.getSong(id)) }.flowOn(Dispatchers.IO) - suspend fun insertSong(songEntity: SongEntity): Flow = - flow { emit(localDataSource.insertSong(songEntity)) }.flowOn(Dispatchers.IO) + suspend fun insertSong(songEntity: SongEntity): Flow = + flow { emit(localDataSource.insertSong(songEntity)) }.flowOn(Dispatchers.IO) - suspend fun updateListenCount(videoId: String) = withContext(Dispatchers.IO) { localDataSource.updateListenCount(videoId) } + suspend fun updateListenCount(videoId: String) = + withContext(Dispatchers.IO) { localDataSource.updateListenCount(videoId) } - suspend fun updateLikeStatus( - videoId: String, - likeStatus: Int, - ) = withContext(Dispatchers.Main) { localDataSource.updateLiked(likeStatus, videoId) } + suspend fun updateLikeStatus( + videoId: String, + likeStatus: Int, + ) = withContext(Dispatchers.Main) { localDataSource.updateLiked(likeStatus, videoId) } - suspend fun updateSongInLibrary( - inLibrary: LocalDateTime, - videoId: String, - ) = withContext(Dispatchers.Main) { localDataSource.updateSongInLibrary(inLibrary, videoId) } + suspend fun updateSongInLibrary( + inLibrary: LocalDateTime, + videoId: String, + ) = withContext(Dispatchers.Main) { localDataSource.updateSongInLibrary(inLibrary, videoId) } - suspend fun updateDurationSeconds( - durationSeconds: Int, - videoId: String, - ) = withContext(Dispatchers.Main) { localDataSource.updateDurationSeconds(durationSeconds, videoId) } - - suspend fun getMostPlayedSongs(): Flow> = - flow { emit(localDataSource.getMostPlayedSongs()) }.flowOn(Dispatchers.IO) + suspend fun updateDurationSeconds( + durationSeconds: Int, + videoId: String, + ) = withContext(Dispatchers.Main) { + localDataSource.updateDurationSeconds( + durationSeconds, + videoId + ) + } - suspend fun updateDownloadState( - videoId: String, - downloadState: Int, - ) = withContext(Dispatchers.Main) { - localDataSource.updateDownloadState( - downloadState, - videoId, - ) - } + suspend fun getMostPlayedSongs(): Flow> = + flow { emit(localDataSource.getMostPlayedSongs()) }.flowOn(Dispatchers.IO) + + suspend fun updateDownloadState( + videoId: String, + downloadState: Int, + ) = withContext(Dispatchers.Main) { + localDataSource.updateDownloadState( + downloadState, + videoId, + ) + } - suspend fun getAllArtists(): Flow> = flow { emit(localDataSource.getAllArtists()) }.flowOn(Dispatchers.IO) + suspend fun getAllArtists(): Flow> = + flow { emit(localDataSource.getAllArtists()) }.flowOn(Dispatchers.IO) - suspend fun getArtistById(id: String): Flow = flow { emit(localDataSource.getArtist(id)) }.flowOn(Dispatchers.IO) + suspend fun getArtistById(id: String): Flow = + flow { emit(localDataSource.getArtist(id)) }.flowOn(Dispatchers.IO) - suspend fun insertArtist(artistEntity: ArtistEntity) = withContext(Dispatchers.IO) { localDataSource.insertArtist(artistEntity) } + suspend fun insertArtist(artistEntity: ArtistEntity) = + withContext(Dispatchers.IO) { localDataSource.insertArtist(artistEntity) } - suspend fun updateFollowedStatus( - channelId: String, - followedStatus: Int, - ) = withContext(Dispatchers.Main) { localDataSource.updateFollowed(followedStatus, channelId) } + suspend fun updateFollowedStatus( + channelId: String, + followedStatus: Int, + ) = withContext(Dispatchers.Main) { localDataSource.updateFollowed(followedStatus, channelId) } - suspend fun getFollowedArtists(): Flow> = - flow { emit(localDataSource.getFollowedArtists()) }.flowOn(Dispatchers.IO) + suspend fun getFollowedArtists(): Flow> = + flow { emit(localDataSource.getFollowedArtists()) }.flowOn(Dispatchers.IO) - suspend fun updateArtistInLibrary( - inLibrary: LocalDateTime, - channelId: String, - ) = withContext(Dispatchers.Main) { - localDataSource.updateArtistInLibrary( - inLibrary, - channelId, - ) - } + suspend fun updateArtistInLibrary( + inLibrary: LocalDateTime, + channelId: String, + ) = withContext(Dispatchers.Main) { + localDataSource.updateArtistInLibrary( + inLibrary, + channelId, + ) + } - suspend fun getAllAlbums(): Flow> = flow { emit(localDataSource.getAllAlbums()) }.flowOn(Dispatchers.IO) + suspend fun getAllAlbums(): Flow> = + flow { emit(localDataSource.getAllAlbums()) }.flowOn(Dispatchers.IO) - suspend fun getAlbum(id: String): Flow = flow { emit(localDataSource.getAlbum(id)) }.flowOn(Dispatchers.IO) + suspend fun getAlbum(id: String): Flow = + flow { emit(localDataSource.getAlbum(id)) }.flowOn(Dispatchers.IO) - suspend fun getLikedAlbums(): Flow> = flow { emit(localDataSource.getLikedAlbums()) }.flowOn(Dispatchers.IO) + suspend fun getLikedAlbums(): Flow> = + flow { emit(localDataSource.getLikedAlbums()) }.flowOn(Dispatchers.IO) - suspend fun insertAlbum(albumEntity: AlbumEntity) = withContext(Dispatchers.IO) { localDataSource.insertAlbum(albumEntity) } + suspend fun insertAlbum(albumEntity: AlbumEntity) = + withContext(Dispatchers.IO) { localDataSource.insertAlbum(albumEntity) } - suspend fun updateAlbumLiked( - albumId: String, - likeStatus: Int, - ) = withContext(Dispatchers.Main) { localDataSource.updateAlbumLiked(likeStatus, albumId) } + suspend fun updateAlbumLiked( + albumId: String, + likeStatus: Int, + ) = withContext(Dispatchers.Main) { localDataSource.updateAlbumLiked(likeStatus, albumId) } - suspend fun updateAlbumInLibrary( - inLibrary: LocalDateTime, - albumId: String, - ) = withContext(Dispatchers.Main) { localDataSource.updateAlbumInLibrary(inLibrary, albumId) } + suspend fun updateAlbumInLibrary( + inLibrary: LocalDateTime, + albumId: String, + ) = withContext(Dispatchers.Main) { localDataSource.updateAlbumInLibrary(inLibrary, albumId) } - suspend fun updateAlbumDownloadState( - albumId: String, - downloadState: Int, - ) = withContext(Dispatchers.Main) { - localDataSource.updateAlbumDownloadState( - downloadState, - albumId, - ) - } + suspend fun updateAlbumDownloadState( + albumId: String, + downloadState: Int, + ) = withContext(Dispatchers.Main) { + localDataSource.updateAlbumDownloadState( + downloadState, + albumId, + ) + } - suspend fun getAllPlaylists(): Flow> = flow { emit(localDataSource.getAllPlaylists()) }.flowOn(Dispatchers.IO) + suspend fun getAllPlaylists(): Flow> = + flow { emit(localDataSource.getAllPlaylists()) }.flowOn(Dispatchers.IO) - suspend fun getPlaylist(id: String): Flow = flow { emit(localDataSource.getPlaylist(id)) }.flowOn(Dispatchers.IO) + suspend fun getPlaylist(id: String): Flow = + flow { emit(localDataSource.getPlaylist(id)) }.flowOn(Dispatchers.IO) - suspend fun getLikedPlaylists(): Flow> = - flow { emit(localDataSource.getLikedPlaylists()) }.flowOn(Dispatchers.IO) + suspend fun getLikedPlaylists(): Flow> = + flow { emit(localDataSource.getLikedPlaylists()) }.flowOn(Dispatchers.IO) - suspend fun insertPlaylist(playlistEntity: PlaylistEntity) = - withContext(Dispatchers.IO) { localDataSource.insertPlaylist(playlistEntity) } + suspend fun insertPlaylist(playlistEntity: PlaylistEntity) = + withContext(Dispatchers.IO) { localDataSource.insertPlaylist(playlistEntity) } - suspend fun insertRadioPlaylist(playlistEntity: PlaylistEntity) = - withContext(Dispatchers.IO) { localDataSource.insertRadioPlaylist(playlistEntity) } + suspend fun insertRadioPlaylist(playlistEntity: PlaylistEntity) = + withContext(Dispatchers.IO) { localDataSource.insertRadioPlaylist(playlistEntity) } - suspend fun updatePlaylistLiked( - playlistId: String, - likeStatus: Int, - ) = withContext(Dispatchers.Main) { - localDataSource.updatePlaylistLiked( - likeStatus, - playlistId, - ) - } - - suspend fun updatePlaylistInLibrary( - inLibrary: LocalDateTime, - playlistId: String, - ) = withContext(Dispatchers.Main) { - localDataSource.updatePlaylistInLibrary( - inLibrary, - playlistId, - ) - } + suspend fun updatePlaylistLiked( + playlistId: String, + likeStatus: Int, + ) = withContext(Dispatchers.Main) { + localDataSource.updatePlaylistLiked( + likeStatus, + playlistId, + ) + } - suspend fun updatePlaylistDownloadState( - playlistId: String, - downloadState: Int, - ) = withContext(Dispatchers.Main) { - localDataSource.updatePlaylistDownloadState( - downloadState, - playlistId, - ) - } + suspend fun updatePlaylistInLibrary( + inLibrary: LocalDateTime, + playlistId: String, + ) = withContext(Dispatchers.Main) { + localDataSource.updatePlaylistInLibrary( + inLibrary, + playlistId, + ) + } - suspend fun getAllLocalPlaylists(): Flow> = - flow { emit(localDataSource.getAllLocalPlaylists()) }.flowOn(Dispatchers.IO) + suspend fun updatePlaylistDownloadState( + playlistId: String, + downloadState: Int, + ) = withContext(Dispatchers.Main) { + localDataSource.updatePlaylistDownloadState( + downloadState, + playlistId, + ) + } - suspend fun getLocalPlaylist(id: Long): Flow = - flow { emit(localDataSource.getLocalPlaylist(id)) }.flowOn(Dispatchers.IO) + suspend fun getAllLocalPlaylists(): Flow> = + flow { emit(localDataSource.getAllLocalPlaylists()) }.flowOn(Dispatchers.IO) + + suspend fun getLocalPlaylist(id: Long): Flow = + flow { emit(localDataSource.getLocalPlaylist(id)) }.flowOn(Dispatchers.IO) + + suspend fun insertLocalPlaylist(localPlaylistEntity: LocalPlaylistEntity) = + withContext(Dispatchers.IO) { localDataSource.insertLocalPlaylist(localPlaylistEntity) } + + suspend fun deleteLocalPlaylist(id: Long) = + withContext(Dispatchers.IO) { localDataSource.deleteLocalPlaylist(id) } + + suspend fun updateLocalPlaylistTitle( + title: String, + id: Long, + ) = withContext(Dispatchers.IO) { localDataSource.updateLocalPlaylistTitle(title, id) } + + suspend fun updateLocalPlaylistThumbnail( + thumbnail: String, + id: Long, + ) = withContext(Dispatchers.IO) { localDataSource.updateLocalPlaylistThumbnail(thumbnail, id) } + + suspend fun updateLocalPlaylistTracks( + tracks: List, + id: Long, + ) = withContext(Dispatchers.IO) { localDataSource.updateLocalPlaylistTracks(tracks, id) } + + suspend fun updateLocalPlaylistDownloadState( + downloadState: Int, + id: Long, + ) = withContext(Dispatchers.IO) { + localDataSource.updateLocalPlaylistDownloadState( + downloadState, + id, + ) + } - suspend fun insertLocalPlaylist(localPlaylistEntity: LocalPlaylistEntity) = - withContext(Dispatchers.IO) { localDataSource.insertLocalPlaylist(localPlaylistEntity) } + suspend fun updateLocalPlaylistInLibrary( + inLibrary: LocalDateTime, + id: Long, + ) = withContext(Dispatchers.IO) { localDataSource.updateLocalPlaylistInLibrary(inLibrary, id) } - suspend fun deleteLocalPlaylist(id: Long) = withContext(Dispatchers.IO) { localDataSource.deleteLocalPlaylist(id) } + suspend fun getDownloadedLocalPlaylists(): Flow> = + flow { emit(localDataSource.getDownloadedLocalPlaylists()) }.flowOn(Dispatchers.IO) - suspend fun updateLocalPlaylistTitle( - title: String, - id: Long, - ) = withContext(Dispatchers.IO) { localDataSource.updateLocalPlaylistTitle(title, id) } + suspend fun getAllRecentData(): Flow> = + flow { emit(localDataSource.getAllRecentData()) }.flowOn(Dispatchers.IO) - suspend fun updateLocalPlaylistThumbnail( - thumbnail: String, - id: Long, - ) = withContext(Dispatchers.IO) { localDataSource.updateLocalPlaylistThumbnail(thumbnail, id) } + suspend fun getAllDownloadedPlaylist(): Flow> = + flow { emit(localDataSource.getAllDownloadedPlaylist()) }.flowOn(Dispatchers.IO) - suspend fun updateLocalPlaylistTracks( - tracks: List, - id: Long, - ) = withContext(Dispatchers.IO) { localDataSource.updateLocalPlaylistTracks(tracks, id) } + suspend fun getRecentSong( + limit: Int, + offset: Int, + ) = localDataSource.getRecentSongs(limit, offset) - suspend fun updateLocalPlaylistDownloadState( - downloadState: Int, - id: Long, - ) = withContext(Dispatchers.IO) { - localDataSource.updateLocalPlaylistDownloadState( - downloadState, - id, - ) - } + suspend fun getSavedLyrics(videoId: String): Flow = + flow { emit(localDataSource.getSavedLyrics(videoId)) }.flowOn(Dispatchers.IO) - suspend fun updateLocalPlaylistInLibrary( - inLibrary: LocalDateTime, - id: Long, - ) = withContext(Dispatchers.IO) { localDataSource.updateLocalPlaylistInLibrary(inLibrary, id) } + suspend fun insertLyrics(lyricsEntity: LyricsEntity) = + withContext(Dispatchers.IO) { localDataSource.insertLyrics(lyricsEntity) } - suspend fun getDownloadedLocalPlaylists(): Flow> = - flow { emit(localDataSource.getDownloadedLocalPlaylists()) }.flowOn(Dispatchers.IO) + suspend fun insertNewFormat(newFormat: NewFormatEntity) = + withContext(Dispatchers.IO) { localDataSource.insertNewFormat(newFormat) } - suspend fun getAllRecentData(): Flow> = flow { emit(localDataSource.getAllRecentData()) }.flowOn(Dispatchers.IO) + suspend fun getNewFormat(videoId: String): Flow = + flow { emit(localDataSource.getNewFormat(videoId)) }.flowOn(Dispatchers.Main) - suspend fun getAllDownloadedPlaylist(): Flow> = - flow { emit(localDataSource.getAllDownloadedPlaylist()) }.flowOn(Dispatchers.IO) + suspend fun insertSongInfo(songInfo: SongInfoEntity) = + withContext(Dispatchers.IO) { + localDataSource.insertSongInfo(songInfo) + } - suspend fun getRecentSong( - limit: Int, - offset: Int, - ) = localDataSource.getRecentSongs(limit, offset) + suspend fun getSongInfoEntiy(videoId: String): Flow = + flow { emit(localDataSource.getSongInfo(videoId)) }.flowOn(Dispatchers.Main) - suspend fun getSavedLyrics(videoId: String): Flow = - flow { emit(localDataSource.getSavedLyrics(videoId)) }.flowOn(Dispatchers.IO) + suspend fun recoverQueue(temp: List) { + val queueEntity = QueueEntity(listTrack = temp) + withContext(Dispatchers.IO) { localDataSource.recoverQueue(queueEntity) } + } - suspend fun insertLyrics(lyricsEntity: LyricsEntity) = withContext(Dispatchers.IO) { localDataSource.insertLyrics(lyricsEntity) } + suspend fun removeQueue() { + withContext(Dispatchers.IO) { localDataSource.deleteQueue() } + } - suspend fun insertNewFormat(newFormat: NewFormatEntity) = withContext(Dispatchers.IO) { localDataSource.insertNewFormat(newFormat) } + suspend fun getSavedQueue(): Flow?> = + flow { emit(localDataSource.getQueue()) }.flowOn(Dispatchers.IO) - suspend fun getNewFormat(videoId: String): Flow = - flow { emit(localDataSource.getNewFormat(videoId)) }.flowOn(Dispatchers.Main) + suspend fun getLocalPlaylistByYoutubePlaylistId(playlistId: String): Flow = + flow { emit(localDataSource.getLocalPlaylistByYoutubePlaylistId(playlistId)) }.flowOn( + Dispatchers.IO + ) - suspend fun insertSongInfo(songInfo: SongInfoEntity) = - withContext(Dispatchers.IO) { - localDataSource.insertSongInfo(songInfo) - } + suspend fun insertSetVideoId(setVideoId: SetVideoIdEntity) = + withContext(Dispatchers.IO) { localDataSource.insertSetVideoId(setVideoId) } - suspend fun getSongInfoEntiy(videoId: String): Flow = - flow { emit(localDataSource.getSongInfo(videoId)) }.flowOn(Dispatchers.Main) + suspend fun getSetVideoId(videoId: String): Flow = + flow { emit(localDataSource.getSetVideoId(videoId)) }.flowOn(Dispatchers.IO) - suspend fun recoverQueue(temp: List) { - val queueEntity = QueueEntity(listTrack = temp) - withContext(Dispatchers.IO) { localDataSource.recoverQueue(queueEntity) } + suspend fun insertPairSongLocalPlaylist(pairSongLocalPlaylist: PairSongLocalPlaylist) = + withContext(Dispatchers.IO) { + localDataSource.insertPairSongLocalPlaylist(pairSongLocalPlaylist) } - suspend fun removeQueue() { - withContext(Dispatchers.IO) { localDataSource.deleteQueue() } - } + suspend fun getPlaylistPairSong(playlistId: Long): Flow?> = + flow?> { + emit(localDataSource.getPlaylistPairSong(playlistId)) + }.flowOn(Dispatchers.IO) - suspend fun getSavedQueue(): Flow?> = flow { emit(localDataSource.getQueue()) }.flowOn(Dispatchers.IO) + suspend fun deletePairSongLocalPlaylist( + playlistId: Long, + videoId: String, + ) = withContext(Dispatchers.IO) { + localDataSource.deletePairSongLocalPlaylist(playlistId, videoId) + } - suspend fun getLocalPlaylistByYoutubePlaylistId(playlistId: String): Flow = - flow { emit(localDataSource.getLocalPlaylistByYoutubePlaylistId(playlistId)) }.flowOn(Dispatchers.IO) + suspend fun updateLocalPlaylistYouTubePlaylistId( + id: Long, + ytId: String?, + ) = withContext(Dispatchers.IO) { + localDataSource.updateLocalPlaylistYouTubePlaylistId(id, ytId) + } - suspend fun insertSetVideoId(setVideoId: SetVideoIdEntity) = - withContext(Dispatchers.IO) { localDataSource.insertSetVideoId(setVideoId) } + suspend fun updateLocalPlaylistYouTubePlaylistSynced( + id: Long, + synced: Int, + ) = withContext(Dispatchers.IO) { + localDataSource.updateLocalPlaylistYouTubePlaylistSynced(id, synced) + } - suspend fun getSetVideoId(videoId: String): Flow = - flow { emit(localDataSource.getSetVideoId(videoId)) }.flowOn(Dispatchers.IO) + suspend fun updateLocalPlaylistYouTubePlaylistSyncState( + id: Long, + syncState: Int, + ) = withContext(Dispatchers.IO) { + localDataSource.updateLocalPlaylistYouTubePlaylistSyncState(id, syncState) + } - suspend fun insertPairSongLocalPlaylist(pairSongLocalPlaylist: PairSongLocalPlaylist) = - withContext(Dispatchers.IO) { - localDataSource.insertPairSongLocalPlaylist(pairSongLocalPlaylist) - } + suspend fun insertGoogleAccount(googleAccountEntity: GoogleAccountEntity) = + withContext(Dispatchers.IO) { + localDataSource.insertGoogleAccount(googleAccountEntity) + } - suspend fun getPlaylistPairSong(playlistId: Long): Flow?> = - flow?> { - emit(localDataSource.getPlaylistPairSong(playlistId)) - }.flowOn(Dispatchers.IO) + suspend fun getGoogleAccounts(): Flow?> = + flow?> { emit(localDataSource.getGoogleAccounts()) }.flowOn( + Dispatchers.IO, + ) - suspend fun deletePairSongLocalPlaylist( - playlistId: Long, - videoId: String, - ) = withContext(Dispatchers.IO) { - localDataSource.deletePairSongLocalPlaylist(playlistId, videoId) - } + suspend fun getUsedGoogleAccount(): Flow = + flow { emit(localDataSource.getUsedGoogleAccount()) }.flowOn( + Dispatchers.IO, + ) - suspend fun updateLocalPlaylistYouTubePlaylistId( - id: Long, - ytId: String?, - ) = withContext(Dispatchers.IO) { - localDataSource.updateLocalPlaylistYouTubePlaylistId(id, ytId) - } + suspend fun deleteGoogleAccount(email: String) = + withContext(Dispatchers.IO) { localDataSource.deleteGoogleAccount(email) } - suspend fun updateLocalPlaylistYouTubePlaylistSynced( - id: Long, - synced: Int, - ) = withContext(Dispatchers.IO) { - localDataSource.updateLocalPlaylistYouTubePlaylistSynced(id, synced) + suspend fun updateGoogleAccountUsed( + email: String, + isUsed: Boolean, + ) = withContext(Dispatchers.IO) { localDataSource.updateGoogleAccountUsed(email, isUsed) } + + suspend fun insertFollowedArtistSingleAndAlbum(followedArtistSingleAndAlbum: FollowedArtistSingleAndAlbum) = + withContext(Dispatchers.IO) { + localDataSource.insertFollowedArtistSingleAndAlbum(followedArtistSingleAndAlbum) } - suspend fun updateLocalPlaylistYouTubePlaylistSyncState( - id: Long, - syncState: Int, - ) = withContext(Dispatchers.IO) { - localDataSource.updateLocalPlaylistYouTubePlaylistSyncState(id, syncState) + suspend fun deleteFollowedArtistSingleAndAlbum(channelId: String) = + withContext(Dispatchers.IO) { + localDataSource.deleteFollowedArtistSingleAndAlbum(channelId) } - suspend fun insertGoogleAccount(googleAccountEntity: GoogleAccountEntity) = - withContext(Dispatchers.IO) { - localDataSource.insertGoogleAccount(googleAccountEntity) + suspend fun getAllFollowedArtistSingleAndAlbums(): Flow?> = + flow { + emit(localDataSource.getAllFollowedArtistSingleAndAlbums()) + }.flowOn(Dispatchers.IO) + + suspend fun getFollowedArtistSingleAndAlbum(channelId: String): Flow = + flow { + emit(localDataSource.getFollowedArtistSingleAndAlbum(channelId)) + }.flowOn(Dispatchers.IO) + + suspend fun getAccountInfo() = + flow { + YouTube.accountInfo().onSuccess { accountInfo -> + emit(accountInfo) + }.onFailure { + it.printStackTrace() + emit(null) } + }.flowOn(Dispatchers.IO) - suspend fun getGoogleAccounts(): Flow?> = - flow?> { emit(localDataSource.getGoogleAccounts()) }.flowOn( - Dispatchers.IO, - ) - - suspend fun getUsedGoogleAccount(): Flow = - flow { emit(localDataSource.getUsedGoogleAccount()) }.flowOn( - Dispatchers.IO, - ) - - suspend fun deleteGoogleAccount(email: String) = withContext(Dispatchers.IO) { localDataSource.deleteGoogleAccount(email) } - - suspend fun updateGoogleAccountUsed( - email: String, - isUsed: Boolean, - ) = withContext(Dispatchers.IO) { localDataSource.updateGoogleAccountUsed(email, isUsed) } - - suspend fun getAccountInfo() = - flow { - YouTube.accountInfo().onSuccess { accountInfo -> - emit(accountInfo) - }.onFailure { - it.printStackTrace() - emit(null) - } - }.flowOn(Dispatchers.IO) - - suspend fun getHomeData(): Flow>> = - flow { - runCatching { - val limit = dataStoreManager.homeLimit.first() - YouTube.customQuery(browseId = "FEmusic_home").onSuccess { result -> - val list: ArrayList = arrayListOf() - if (result.contents?.singleColumnBrowseResultsRenderer?.tabs?.get( + suspend fun getHomeData(): Flow>> = + flow { + runCatching { + val limit = dataStoreManager.homeLimit.first() + YouTube.customQuery(browseId = "FEmusic_home").onSuccess { result -> + val list: ArrayList = arrayListOf() + if (result.contents?.singleColumnBrowseResultsRenderer?.tabs?.get( + 0, + )?.tabRenderer?.content?.sectionListRenderer?.contents?.get( + 0, + )?.musicCarouselShelfRenderer?.header?.musicCarouselShelfBasicHeaderRenderer?.strapline?.runs?.get( + 0, + )?.text != null + ) { + val accountName = + result.contents?.singleColumnBrowseResultsRenderer?.tabs?.get( 0, )?.tabRenderer?.content?.sectionListRenderer?.contents?.get( 0, )?.musicCarouselShelfRenderer?.header?.musicCarouselShelfBasicHeaderRenderer?.strapline?.runs?.get( 0, - )?.text != null - ) { - val accountName = - result.contents?.singleColumnBrowseResultsRenderer?.tabs?.get( - 0, - )?.tabRenderer?.content?.sectionListRenderer?.contents?.get( - 0, - )?.musicCarouselShelfRenderer?.header?.musicCarouselShelfBasicHeaderRenderer?.strapline?.runs?.get( - 0, - )?.text ?: "" - val accountThumbUrl = - result.contents?.singleColumnBrowseResultsRenderer?.tabs?.get( - 0, - )?.tabRenderer?.content?.sectionListRenderer?.contents?.get( - 0, - )?.musicCarouselShelfRenderer?.header?.musicCarouselShelfBasicHeaderRenderer?.thumbnail?.musicThumbnailRenderer?.thumbnail?.thumbnails?.get( - 0, - )?.url?.replace("s88", "s352") ?: "" - if (accountName != "" && accountThumbUrl != "") { - dataStoreManager.putString("AccountName", accountName) - dataStoreManager.putString("AccountThumbUrl", accountThumbUrl) - } - } - var continueParam = + )?.text ?: "" + val accountThumbUrl = result.contents?.singleColumnBrowseResultsRenderer?.tabs?.get( 0, - )?.tabRenderer?.content?.sectionListRenderer?.continuations?.get( + )?.tabRenderer?.content?.sectionListRenderer?.contents?.get( 0, - )?.nextContinuationData?.continuation - val data = - result.contents?.singleColumnBrowseResultsRenderer?.tabs?.get( + )?.musicCarouselShelfRenderer?.header?.musicCarouselShelfBasicHeaderRenderer?.thumbnail?.musicThumbnailRenderer?.thumbnail?.thumbnails?.get( 0, - )?.tabRenderer?.content?.sectionListRenderer?.contents - list.addAll(parseMixedContent(data, context)) - var count = 0 - while (count < limit && continueParam != null) { - YouTube.customQuery(browseId = "", continuation = continueParam) - .onSuccess { response -> - continueParam = - response.continuationContents?.sectionListContinuation?.continuations?.get( - 0, - )?.nextContinuationData?.continuation - Log.d("Repository", "continueParam: $continueParam") - val dataContinue = - response.continuationContents?.sectionListContinuation?.contents - list.addAll(parseMixedContent(dataContinue, context)) - count++ - Log.d("Repository", "count: $count") - }.onFailure { - Log.e("Repository", "Error: ${it.message}") - count++ - } + )?.url?.replace("s88", "s352") ?: "" + if (accountName != "" && accountThumbUrl != "") { + dataStoreManager.putString("AccountName", accountName) + dataStoreManager.putString("AccountThumbUrl", accountThumbUrl) } - Log.d("Repository", "List size: ${list.size}") - emit(Resource.Success>(list)) - }.onFailure { error -> - emit(Resource.Error>(error.message.toString())) } - } - }.flowOn(Dispatchers.IO) - - suspend fun getNewRelease(): Flow>> = - flow { - YouTube.newRelease().onSuccess { result -> - emit(Resource.Success>(parseNewRelease(result, context))) + var continueParam = + result.contents?.singleColumnBrowseResultsRenderer?.tabs?.get( + 0, + )?.tabRenderer?.content?.sectionListRenderer?.continuations?.get( + 0, + )?.nextContinuationData?.continuation + val data = + result.contents?.singleColumnBrowseResultsRenderer?.tabs?.get( + 0, + )?.tabRenderer?.content?.sectionListRenderer?.contents + list.addAll(parseMixedContent(data, context)) + var count = 0 + while (count < limit && continueParam != null) { + YouTube.customQuery(browseId = "", continuation = continueParam) + .onSuccess { response -> + continueParam = + response.continuationContents?.sectionListContinuation?.continuations?.get( + 0, + )?.nextContinuationData?.continuation + Log.d("Repository", "continueParam: $continueParam") + val dataContinue = + response.continuationContents?.sectionListContinuation?.contents + list.addAll(parseMixedContent(dataContinue, context)) + count++ + Log.d("Repository", "count: $count") + }.onFailure { + Log.e("Repository", "Error: ${it.message}") + count++ + } + } + Log.d("Repository", "List size: ${list.size}") + emit(Resource.Success>(list)) }.onFailure { error -> emit(Resource.Error>(error.message.toString())) } - }.flowOn(Dispatchers.IO) + } + }.flowOn(Dispatchers.IO) + + suspend fun getNewRelease(): Flow>> = + flow { + YouTube.newRelease().onSuccess { result -> + emit(Resource.Success>(parseNewRelease(result, context))) + }.onFailure { error -> + emit(Resource.Error>(error.message.toString())) + } + }.flowOn(Dispatchers.IO) - suspend fun getChartData(countryCode: String = "KR"): Flow> = - flow { - runCatching { - YouTube.customQuery("FEmusic_charts", country = countryCode).onSuccess { result -> - val data = - result.contents?.singleColumnBrowseResultsRenderer?.tabs?.get(0)?.tabRenderer?.content?.sectionListRenderer - val chart = parseChart(data) - if (chart != null) { - emit(Resource.Success(chart)) - } else { - emit(Resource.Error("Error")) - } - }.onFailure { error -> - emit(Resource.Error(error.message.toString())) + suspend fun getChartData(countryCode: String = "KR"): Flow> = + flow { + runCatching { + YouTube.customQuery("FEmusic_charts", country = countryCode).onSuccess { result -> + val data = + result.contents?.singleColumnBrowseResultsRenderer?.tabs?.get(0)?.tabRenderer?.content?.sectionListRenderer + val chart = parseChart(data) + if (chart != null) { + emit(Resource.Success(chart)) + } else { + emit(Resource.Error("Error")) } + }.onFailure { error -> + emit(Resource.Error(error.message.toString())) } - }.flowOn(Dispatchers.IO) + } + }.flowOn(Dispatchers.IO) - suspend fun getMoodAndMomentsData(): Flow> = - flow { - runCatching { - YouTube.moodAndGenres().onSuccess { result -> - val listMoodMoments: ArrayList = arrayListOf() - val listGenre: ArrayList = arrayListOf() - result[0].let { moodsmoment -> - for (item in moodsmoment.items) { - listMoodMoments.add( - MoodsMoment( - params = item.endpoint.params ?: "", - title = item.title, - ), - ) - } + suspend fun getMoodAndMomentsData(): Flow> = + flow { + runCatching { + YouTube.moodAndGenres().onSuccess { result -> + val listMoodMoments: ArrayList = arrayListOf() + val listGenre: ArrayList = arrayListOf() + result[0].let { moodsmoment -> + for (item in moodsmoment.items) { + listMoodMoments.add( + MoodsMoment( + params = item.endpoint.params ?: "", + title = item.title, + ), + ) } - result[1].let { genres -> - for (item in genres.items) { - listGenre.add( - Genre( - params = item.endpoint.params ?: "", - title = item.title, - ), - ) - } + } + result[1].let { genres -> + for (item in genres.items) { + listGenre.add( + Genre( + params = item.endpoint.params ?: "", + title = item.title, + ), + ) } - emit(Resource.Success(Mood(listGenre, listMoodMoments))) - }.onFailure { e -> - emit(Resource.Error(e.message.toString())) } + emit(Resource.Success(Mood(listGenre, listMoodMoments))) + }.onFailure { e -> + emit(Resource.Error(e.message.toString())) } - }.flowOn(Dispatchers.IO) + } + }.flowOn(Dispatchers.IO) - suspend fun getMoodData(params: String): Flow> = - flow { - runCatching { - YouTube.customQuery(browseId = "FEmusic_moods_and_genres_category", params = params) - .onSuccess { result -> - val data = parseMoodsMomentObject(result) - if (data != null) { - emit(Resource.Success(data)) - } else { - emit(Resource.Error("Error")) - } - } - .onFailure { e -> - emit(Resource.Error(e.message.toString())) - } - } - }.flowOn(Dispatchers.IO) - - suspend fun getGenreData(params: String): Flow> = - flow { - kotlin.runCatching { - YouTube.customQuery(browseId = "FEmusic_moods_and_genres_category", params = params) - .onSuccess { result -> - val data = parseGenreObject(result) - if (data != null) { - emit(Resource.Success(data)) - } else { - emit(Resource.Error("Error")) - } + suspend fun getMoodData(params: String): Flow> = + flow { + runCatching { + YouTube.customQuery(browseId = "FEmusic_moods_and_genres_category", params = params) + .onSuccess { result -> + val data = parseMoodsMomentObject(result) + if (data != null) { + emit(Resource.Success(data)) + } else { + emit(Resource.Error("Error")) } - .onFailure { e -> - emit(Resource.Error(e.message.toString())) + } + .onFailure { e -> + emit(Resource.Error(e.message.toString())) + } + } + }.flowOn(Dispatchers.IO) + + suspend fun getGenreData(params: String): Flow> = + flow { + kotlin.runCatching { + YouTube.customQuery(browseId = "FEmusic_moods_and_genres_category", params = params) + .onSuccess { result -> + val data = parseGenreObject(result) + if (data != null) { + emit(Resource.Success(data)) + } else { + emit(Resource.Error("Error")) } - } - }.flowOn(Dispatchers.IO) + } + .onFailure { e -> + emit(Resource.Error(e.message.toString())) + } + } + }.flowOn(Dispatchers.IO) - suspend fun getContinueTrack( - playlistId: String, - continuation: String, - ): Flow?> = - flow { - runCatching { - Queue.setContinuation(null) - YouTube.next(WatchEndpoint(playlistId = playlistId), continuation = continuation) - .onSuccess { next -> - val data: ArrayList = arrayListOf() - data.addAll(next.items) - val nextContinuation = next.continuation - if (nextContinuation != null) { - Queue.setContinuation(Pair(playlistId, nextContinuation)) - } else { - Queue.setContinuation(null) - } - emit(data.toListTrack()) - }.onFailure { exception -> - exception.printStackTrace() + suspend fun getContinueTrack( + playlistId: String, + continuation: String, + ): Flow?> = + flow { + runCatching { + Queue.setContinuation(null) + YouTube.next(WatchEndpoint(playlistId = playlistId), continuation = continuation) + .onSuccess { next -> + val data: ArrayList = arrayListOf() + data.addAll(next.items) + val nextContinuation = next.continuation + if (nextContinuation != null) { + Queue.setContinuation(Pair(playlistId, nextContinuation)) + } else { Queue.setContinuation(null) - emit(null) } - } + emit(data.toListTrack()) + }.onFailure { exception -> + exception.printStackTrace() + Queue.setContinuation(null) + emit(null) + } } + } - suspend fun getRadio( - radioId: String, - originalTrack: SongEntity? = null, - artist: ArtistEntity? = null, - ): Flow> = - flow { - runCatching { - YouTube.next(endpoint = WatchEndpoint(playlistId = radioId)).onSuccess { next -> - val data: ArrayList = arrayListOf() - data.addAll(next.items) - var continuation = next.continuation - Log.w("Radio", "data: ${data.size}") - var count = 0 - while (continuation != null && count < 3) { - YouTube.next( - endpoint = WatchEndpoint(playlistId = radioId), - continuation = continuation, - ).onSuccess { nextContinue -> - data.addAll(nextContinue.items) - continuation = nextContinue.continuation - if (data.size >= 50) { - val nextContinuation = nextContinue.continuation - if (nextContinuation != null) { - Queue.setContinuation(Pair(radioId, nextContinuation)) - } - continuation = null + suspend fun getRadio( + radioId: String, + originalTrack: SongEntity? = null, + artist: ArtistEntity? = null, + ): Flow> = + flow { + runCatching { + YouTube.next(endpoint = WatchEndpoint(playlistId = radioId)).onSuccess { next -> + val data: ArrayList = arrayListOf() + data.addAll(next.items) + var continuation = next.continuation + Log.w("Radio", "data: ${data.size}") + var count = 0 + while (continuation != null && count < 3) { + YouTube.next( + endpoint = WatchEndpoint(playlistId = radioId), + continuation = continuation, + ).onSuccess { nextContinue -> + data.addAll(nextContinue.items) + continuation = nextContinue.continuation + if (data.size >= 50) { + val nextContinuation = nextContinue.continuation + if (nextContinuation != null) { + Queue.setContinuation(Pair(radioId, nextContinuation)) } - Log.w("Radio", "data: ${data.size}") - count++ - if (count == 3) { - val nextContinuation = nextContinue.continuation - if (nextContinuation != null) { - Queue.setContinuation(Pair(radioId, nextContinuation)) - } + continuation = null + } + Log.w("Radio", "data: ${data.size}") + count++ + if (count == 3) { + val nextContinuation = nextContinue.continuation + if (nextContinuation != null) { + Queue.setContinuation(Pair(radioId, nextContinuation)) } - }.onFailure { - if (count == 3) { - val nextContinuation = continuation - if (nextContinuation != null) { - Queue.setContinuation(Pair(radioId, nextContinuation)) - } + } + }.onFailure { + if (count == 3) { + val nextContinuation = continuation + if (nextContinuation != null) { + Queue.setContinuation(Pair(radioId, nextContinuation)) } - continuation = null - count++ } + continuation = null + count++ } - val listTrackResult = data.toListTrack() - if (originalTrack != null) { - listTrackResult.add(0, originalTrack.toTrack()) - } - Log.w("Repository", "data: ${data.size}") - val playlistBrowse = - PlaylistBrowse( - author = Author(id = "", name = "YouTube Music"), - description = context.getString(R.string.auto_created_by_youtube_music), - duration = "", - durationSeconds = 0, - id = radioId, - privacy = "PRIVATE", - thumbnails = - listOf( - Thumbnail( - 544, - originalTrack?.thumbnails ?: artist?.thumbnails ?: "", - 544, - ), - ), - title = "${originalTrack?.title ?: artist?.name} ${context.getString(R.string.radio)}", - trackCount = listTrackResult.size, - tracks = listTrackResult, - year = LocalDateTime.now().year.toString(), - ) - Log.w("Repository", "playlistBrowse: $playlistBrowse") - emit(Resource.Success(playlistBrowse)) } - .onFailure { - exception -> - exception.printStackTrace() - emit(Resource.Error(exception.message.toString())) - } + val listTrackResult = data.toListTrack() + if (originalTrack != null) { + listTrackResult.add(0, originalTrack.toTrack()) + } + Log.w("Repository", "data: ${data.size}") + val playlistBrowse = + PlaylistBrowse( + author = Author(id = "", name = "YouTube Music"), + description = context.getString(R.string.auto_created_by_youtube_music), + duration = "", + durationSeconds = 0, + id = radioId, + privacy = "PRIVATE", + thumbnails = + listOf( + Thumbnail( + 544, + originalTrack?.thumbnails ?: artist?.thumbnails ?: "", + 544, + ), + ), + title = "${originalTrack?.title ?: artist?.name} ${context.getString(R.string.radio)}", + trackCount = listTrackResult.size, + tracks = listTrackResult, + year = LocalDateTime.now().year.toString(), + ) + Log.w("Repository", "playlistBrowse: $playlistBrowse") + emit(Resource.Success(playlistBrowse)) } - }.flowOn(Dispatchers.IO) + .onFailure { exception -> + exception.printStackTrace() + emit(Resource.Error(exception.message.toString())) + } + } + }.flowOn(Dispatchers.IO) - suspend fun reloadSuggestionPlaylist(reloadParams: String): Flow?>?> = - flow { - runCatching { - YouTube.customQuery(browseId = "", continuation = reloadParams, setLogin = true).onSuccess { values -> + suspend fun reloadSuggestionPlaylist(reloadParams: String): Flow?>?> = + flow { + runCatching { + YouTube.customQuery(browseId = "", continuation = reloadParams, setLogin = true) + .onSuccess { values -> val data = values.continuationContents?.musicShelfContinuation?.contents - val dataResult: ArrayList = arrayListOf() + val dataResult: ArrayList = + arrayListOf() if (!data.isNullOrEmpty()) { dataResult.addAll(data) } @@ -727,350 +779,392 @@ class MainRepository dataResult.forEach { listTrack.add( ( - SearchPage.toYTItem( - it.musicResponsiveListItemRenderer, - ) as SongItem - ).toTrack(), + SearchPage.toYTItem( + it.musicResponsiveListItemRenderer, + ) as SongItem + ).toTrack(), ) } emit(Pair(reloadParamsNew, listTrack)) } else { emit(null) } - }.onFailure { - exception -> + }.onFailure { exception -> exception.printStackTrace() emit(null) } - } } + } - suspend fun getSuggestionPlaylist(ytPlaylistId: String): Flow?>?> = - flow { - runCatching { - var id = "" - if (!ytPlaylistId.startsWith("VL")) { - id += "VL$ytPlaylistId" - } else { - id += ytPlaylistId - } - YouTube.customQuery(browseId = id, setLogin = true).onSuccess { result -> - println(result) - var continueParam = - result.contents?.singleColumnBrowseResultsRenderer?.tabs?.get( - 0, - )?.tabRenderer?.content?.sectionListRenderer?.contents?.get( - 0, - )?.musicPlaylistShelfRenderer?.continuations?.get( - 0, - )?.nextContinuationData?.continuation ?: result.contents?.singleColumnBrowseResultsRenderer?.tabs?.get( + suspend fun getSuggestionPlaylist(ytPlaylistId: String): Flow?>?> = + flow { + runCatching { + var id = "" + if (!ytPlaylistId.startsWith("VL")) { + id += "VL$ytPlaylistId" + } else { + id += ytPlaylistId + } + YouTube.customQuery(browseId = id, setLogin = true).onSuccess { result -> + println(result) + var continueParam = + result.contents?.singleColumnBrowseResultsRenderer?.tabs?.get( + 0, + )?.tabRenderer?.content?.sectionListRenderer?.contents?.get( + 0, + )?.musicPlaylistShelfRenderer?.continuations?.get( + 0, + )?.nextContinuationData?.continuation + ?: result.contents?.singleColumnBrowseResultsRenderer?.tabs?.get( 0, )?.tabRenderer?.content?.sectionListRenderer?.continuations?.get(0)?.nextContinuationData?.continuation - val dataResult: ArrayList = arrayListOf() - var reloadParams: String? = null - println("continueParam: $continueParam") - while (continueParam != null) { - YouTube.customQuery(browseId = "", continuation = continueParam, setLogin = true).onSuccess { values -> - val data = - values.continuationContents?.sectionListContinuation?.contents?.get( - 0, - )?.musicShelfRenderer?.contents - println("data: $data") - if (!data.isNullOrEmpty()) { - dataResult.addAll(data) - } - reloadParams = - values.continuationContents?.sectionListContinuation?.contents?.get( - 0, - )?.musicShelfRenderer?.continuations?.get(0)?.reloadContinuationData?.continuation - continueParam = - values.continuationContents?.musicPlaylistShelfContinuation?.continuations?.get( - 0, - )?.nextContinuationData?.continuation - println("reloadParams: $reloadParams") - println("continueParam: $continueParam") - }.onFailure { - Log.e("Repository", "Error: ${it.message}") - continueParam = null + val dataResult: ArrayList = arrayListOf() + var reloadParams: String? = null + println("continueParam: $continueParam") + while (continueParam != null) { + YouTube.customQuery( + browseId = "", + continuation = continueParam, + setLogin = true + ).onSuccess { values -> + val data = + values.continuationContents?.sectionListContinuation?.contents?.get( + 0, + )?.musicShelfRenderer?.contents + println("data: $data") + if (!data.isNullOrEmpty()) { + dataResult.addAll(data) } + reloadParams = + values.continuationContents?.sectionListContinuation?.contents?.get( + 0, + )?.musicShelfRenderer?.continuations?.get(0)?.reloadContinuationData?.continuation + continueParam = + values.continuationContents?.musicPlaylistShelfContinuation?.continuations?.get( + 0, + )?.nextContinuationData?.continuation + println("reloadParams: $reloadParams") + println("continueParam: $continueParam") + }.onFailure { + Log.e("Repository", "Error: ${it.message}") + continueParam = null } - println("dataResult: ${dataResult.size}") - if (dataResult.isNotEmpty()) { - val listTrack: ArrayList = arrayListOf() - dataResult.forEach { - listTrack.add( - ( + } + println("dataResult: ${dataResult.size}") + if (dataResult.isNotEmpty()) { + val listTrack: ArrayList = arrayListOf() + dataResult.forEach { + listTrack.add( + ( PlaylistPage.fromMusicResponsiveListItemRenderer( it.musicResponsiveListItemRenderer, ) as SongItem - ).toTrack(), - ) - } - println("listTrack: $listTrack") - emit(Pair(reloadParams, listTrack)) - } else { - emit(null) - } - } - .onFailure { exception -> - exception.printStackTrace() - emit(null) - } - } - }.flowOn(Dispatchers.IO) - - suspend fun getPodcastData(podcastId: String): Flow> = - flow { - runCatching { - YouTube.customQuery(browseId = podcastId).onSuccess { result -> - val listEpisode = arrayListOf() - val thumbnail = - result.background?.musicThumbnailRenderer?.thumbnail?.thumbnails?.toListThumbnail() - val title = - result.contents?.twoColumnBrowseResultsRenderer?.tabs?.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents?.firstOrNull()?.musicResponsiveHeaderRenderer?.title?.runs?.firstOrNull()?.text - val author = - result.contents?.twoColumnBrowseResultsRenderer?.tabs?.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents?.firstOrNull()?.musicResponsiveHeaderRenderer?.let { - Artist( - id = it.straplineTextOne?.runs?.firstOrNull()?.navigationEndpoint?.browseEndpoint?.browseId, - name = it.straplineTextOne?.runs?.firstOrNull()?.text ?: "", - ) - } - val authorThumbnail = - result.contents?.twoColumnBrowseResultsRenderer?.tabs?.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents?.firstOrNull()?.musicResponsiveHeaderRenderer?.let { - it.straplineThumbnail?.musicThumbnailRenderer?.thumbnail?.thumbnails?.lastOrNull()?.url - } - val description = - result.contents?.twoColumnBrowseResultsRenderer?.tabs?.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents?.firstOrNull()?.musicResponsiveHeaderRenderer?.description?.musicDescriptionShelfRenderer?.description?.runs?.mapNotNull { - it.text - }?.joinToString("") - val data = - result.contents?.twoColumnBrowseResultsRenderer?.secondaryContents?.sectionListRenderer?.contents?.firstOrNull()?.musicShelfRenderer?.contents - parsePodcastData(data, author).let { - listEpisode.addAll(it) - } - var continueParam = - result.contents?.twoColumnBrowseResultsRenderer?.secondaryContents?.sectionListRenderer?.contents?.firstOrNull()?.musicShelfRenderer?.continuations?.firstOrNull()?.nextContinuationData?.continuation - while (continueParam != null) { - YouTube.customQuery(continuation = continueParam, browseId = "") - .onSuccess { continueData -> - parsePodcastContinueData( - continueData.continuationContents?.musicShelfContinuation?.contents, - author, - ).let { - listEpisode.addAll(it) - } - continueParam = - continueData.continuationContents?.musicShelfContinuation?.continuations?.firstOrNull()?.nextContinuationData?.continuation - } - .onFailure { - it.printStackTrace() - continueParam = null - } - } - if (author != null) { - emit( - Resource.Success( - PodcastBrowse( - title = title ?: "", - author = author, - authorThumbnail = authorThumbnail, - thumbnail = thumbnail ?: emptyList(), - description = description, - listEpisode = listEpisode, - ), - ), + ).toTrack(), ) - } else { - emit(Resource.Error("Error")) } - }.onFailure { error -> - emit(Resource.Error(error.message.toString())) + println("listTrack: $listTrack") + emit(Pair(reloadParams, listTrack)) + } else { + emit(null) } } + .onFailure { exception -> + exception.printStackTrace() + emit(null) + } } + }.flowOn(Dispatchers.IO) - suspend fun getPlaylistData(playlistId: String): Flow> = - flow { - runCatching { - var id = "" - id += - if (!playlistId.startsWith("VL")) { - "VL$playlistId" - } else { - playlistId + suspend fun getPodcastData(podcastId: String): Flow> = + flow { + runCatching { + YouTube.customQuery(browseId = podcastId).onSuccess { result -> + val listEpisode = arrayListOf() + val thumbnail = + result.background?.musicThumbnailRenderer?.thumbnail?.thumbnails?.toListThumbnail() + val title = + result.contents?.twoColumnBrowseResultsRenderer?.tabs?.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents?.firstOrNull()?.musicResponsiveHeaderRenderer?.title?.runs?.firstOrNull()?.text + val author = + result.contents?.twoColumnBrowseResultsRenderer?.tabs?.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents?.firstOrNull()?.musicResponsiveHeaderRenderer?.let { + Artist( + id = it.straplineTextOne?.runs?.firstOrNull()?.navigationEndpoint?.browseEndpoint?.browseId, + name = it.straplineTextOne?.runs?.firstOrNull()?.text ?: "", + ) } - Log.d("Repository", "playlist id: $id") - YouTube.customQuery(browseId = id, setLogin = true).onSuccess { result -> - val listContent: ArrayList = arrayListOf() - val data: List? = - result.contents?.singleColumnBrowseResultsRenderer?.tabs?.get( - 0, - )?.tabRenderer?.content?.sectionListRenderer?.contents?.get( - 0, - )?.musicPlaylistShelfRenderer?.contents - if (data != null) { - Log.d("Data", "data: $data") - Log.d("Data", "data size: ${data.size}") - listContent.addAll(data) + val authorThumbnail = + result.contents?.twoColumnBrowseResultsRenderer?.tabs?.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents?.firstOrNull()?.musicResponsiveHeaderRenderer?.let { + it.straplineThumbnail?.musicThumbnailRenderer?.thumbnail?.thumbnails?.lastOrNull()?.url } - val header = result.header?.musicDetailHeaderRenderer ?: result.header?.musicEditablePlaylistDetailHeaderRenderer - Log.d("Header", "header: $header") - var continueParam = - result.contents?.singleColumnBrowseResultsRenderer?.tabs?.get( - 0, - )?.tabRenderer?.content?.sectionListRenderer?.contents?.get( - 0, - )?.musicPlaylistShelfRenderer?.continuations?.get(0)?.nextContinuationData?.continuation - var count = 0 - Log.d("Repository", "playlist data: ${listContent.size}") - Log.d("Repository", "continueParam: $continueParam") - while (continueParam != null) { - YouTube.customQuery(browseId = "", continuation = continueParam, setLogin = true).onSuccess { values -> - Log.d("Continue", "continue: $continueParam") - val dataMore: List? = values.continuationContents?.musicPlaylistShelfContinuation?.contents - if (dataMore != null) { - listContent.addAll(dataMore) + val description = + result.contents?.twoColumnBrowseResultsRenderer?.tabs?.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents?.firstOrNull()?.musicResponsiveHeaderRenderer?.description?.musicDescriptionShelfRenderer?.description?.runs?.mapNotNull { + it.text + }?.joinToString("") + val data = + result.contents?.twoColumnBrowseResultsRenderer?.secondaryContents?.sectionListRenderer?.contents?.firstOrNull()?.musicShelfRenderer?.contents + parsePodcastData(data, author).let { + listEpisode.addAll(it) + } + var continueParam = + result.contents?.twoColumnBrowseResultsRenderer?.secondaryContents?.sectionListRenderer?.contents?.firstOrNull()?.musicShelfRenderer?.continuations?.firstOrNull()?.nextContinuationData?.continuation + while (continueParam != null) { + YouTube.customQuery(continuation = continueParam, browseId = "") + .onSuccess { continueData -> + parsePodcastContinueData( + continueData.continuationContents?.musicShelfContinuation?.contents, + author, + ).let { + listEpisode.addAll(it) } continueParam = - values.continuationContents?.musicPlaylistShelfContinuation?.continuations?.get( - 0, - )?.nextContinuationData?.continuation - count++ - }.onFailure { - Log.e("Continue", "Error: ${it.message}") + continueData.continuationContents?.musicShelfContinuation?.continuations?.firstOrNull()?.nextContinuationData?.continuation + } + .onFailure { + it.printStackTrace() continueParam = null - count++ } + } + if (author != null) { + emit( + Resource.Success( + PodcastBrowse( + title = title ?: "", + author = author, + authorThumbnail = authorThumbnail, + thumbnail = thumbnail ?: emptyList(), + description = description, + listEpisode = listEpisode, + ), + ), + ) + } else { + emit(Resource.Error("Error")) + } + }.onFailure { error -> + emit(Resource.Error(error.message.toString())) + } + } + } + + suspend fun getPlaylistData(playlistId: String): Flow> = + flow { + runCatching { + var id = "" + id += + if (!playlistId.startsWith("VL")) { + "VL$playlistId" + } else { + playlistId + } + Log.d("Repository", "playlist id: $id") + YouTube.customQuery(browseId = id, setLogin = true).onSuccess { result -> + val listContent: ArrayList = arrayListOf() + val data: List? = + result.contents?.singleColumnBrowseResultsRenderer?.tabs?.get( + 0, + )?.tabRenderer?.content?.sectionListRenderer?.contents?.get( + 0, + )?.musicPlaylistShelfRenderer?.contents + if (data != null) { + Log.d("Data", "data: $data") + Log.d("Data", "data size: ${data.size}") + listContent.addAll(data) + } + val header = result.header?.musicDetailHeaderRenderer + ?: result.header?.musicEditablePlaylistDetailHeaderRenderer + Log.d("Header", "header: $header") + var continueParam = + result.contents?.singleColumnBrowseResultsRenderer?.tabs?.get( + 0, + )?.tabRenderer?.content?.sectionListRenderer?.contents?.get( + 0, + )?.musicPlaylistShelfRenderer?.continuations?.get(0)?.nextContinuationData?.continuation + var count = 0 + Log.d("Repository", "playlist data: ${listContent.size}") + Log.d("Repository", "continueParam: $continueParam") + while (continueParam != null) { + YouTube.customQuery( + browseId = "", + continuation = continueParam, + setLogin = true + ).onSuccess { values -> + Log.d("Continue", "continue: $continueParam") + val dataMore: List? = + values.continuationContents?.musicPlaylistShelfContinuation?.contents + if (dataMore != null) { + listContent.addAll(dataMore) + } + continueParam = + values.continuationContents?.musicPlaylistShelfContinuation?.continuations?.get( + 0, + )?.nextContinuationData?.continuation + count++ + }.onFailure { + Log.e("Continue", "Error: ${it.message}") + continueParam = null + count++ } - Log.d("Repository", "playlist final data: ${listContent.size}") - parsePlaylistData(header, listContent, playlistId, context)?.let { playlist -> - emit(Resource.Success(playlist)) - } ?: emit(Resource.Error("Error")) - }.onFailure { e -> - emit(Resource.Error(e.message.toString())) } + Log.d("Repository", "playlist final data: ${listContent.size}") + parsePlaylistData(header, listContent, playlistId, context)?.let { playlist -> + emit(Resource.Success(playlist)) + } ?: emit(Resource.Error("Error")) + }.onFailure { e -> + emit(Resource.Error(e.message.toString())) } - }.flowOn(Dispatchers.IO) + } + }.flowOn(Dispatchers.IO) - suspend fun getAlbumData(browseId: String): Flow> = - flow { - runCatching { - YouTube.album(browseId, withSongs = true).onSuccess { result -> - emit(Resource.Success(parseAlbumData(result))) - }.onFailure { e -> - Log.d("Album", "Error: ${e.message}") - emit(Resource.Error(e.message.toString())) - } + suspend fun getAlbumData(browseId: String): Flow> = + flow { + runCatching { + YouTube.album(browseId, withSongs = true).onSuccess { result -> + emit(Resource.Success(parseAlbumData(result))) + }.onFailure { e -> + Log.d("Album", "Error: ${e.message}") + emit(Resource.Error(e.message.toString())) } - }.flowOn(Dispatchers.IO) + } + }.flowOn(Dispatchers.IO) - suspend fun getAlbumMore( - browseId: String, - params: String, - ): Flow = - flow { - runCatching { - YouTube.browse(browseId = browseId, params = params).onSuccess { result -> - Log.w("Album More", "result: $result") - emit(result) - }.onFailure { - it.printStackTrace() - emit(null) - } + suspend fun getAlbumMore( + browseId: String, + params: String, + ): Flow = + flow { + runCatching { + YouTube.browse(browseId = browseId, params = params).onSuccess { result -> + Log.w("Album More", "result: $result") + emit(result) + }.onFailure { + it.printStackTrace() + emit(null) } - }.flowOn(Dispatchers.IO) + } + }.flowOn(Dispatchers.IO) - suspend fun getArtistData(channelId: String): Flow> = - flow { - runCatching { - YouTube.artist(channelId).onSuccess { result -> - emit(Resource.Success(parseArtistData(result, context))) - }.onFailure { e -> - Log.d("Artist", "Error: ${e.message}") - emit(Resource.Error(e.message.toString())) - } + suspend fun getArtistData(channelId: String): Flow> = + flow { + runCatching { + YouTube.artist(channelId).onSuccess { result -> + emit(Resource.Success(parseArtistData(result, context))) + }.onFailure { e -> + Log.d("Artist", "Error: ${e.message}") + emit(Resource.Error(e.message.toString())) } - }.flowOn(Dispatchers.IO) + } + }.flowOn(Dispatchers.IO) - suspend fun getSearchDataSong(query: String): Flow>> = - flow { - runCatching { - YouTube.search(query, YouTube.SearchFilter.FILTER_SONG).onSuccess { result -> - val listSongs: ArrayList = arrayListOf() - var countinueParam = result.continuation - parseSearchSong(result).let { list -> - listSongs.addAll(list) - } - var count = 0 - while (count < 2 && countinueParam != null) { - YouTube.searchContinuation(countinueParam).onSuccess { values -> - parseSearchSong(values).let { list -> - listSongs.addAll(list) - } - count++ - countinueParam = values.continuation - }.onFailure { - Log.e("Continue", "Error: ${it.message}") - countinueParam = null - count++ + suspend fun getSearchDataSong(query: String): Flow>> = + flow { + runCatching { + YouTube.search(query, YouTube.SearchFilter.FILTER_SONG).onSuccess { result -> + val listSongs: ArrayList = arrayListOf() + var countinueParam = result.continuation + parseSearchSong(result).let { list -> + listSongs.addAll(list) + } + var count = 0 + while (count < 2 && countinueParam != null) { + YouTube.searchContinuation(countinueParam).onSuccess { values -> + parseSearchSong(values).let { list -> + listSongs.addAll(list) } + count++ + countinueParam = values.continuation + }.onFailure { + Log.e("Continue", "Error: ${it.message}") + countinueParam = null + count++ } - - emit(Resource.Success>(listSongs)) - }.onFailure { e -> - Log.d("Search", "Error: ${e.message}") - emit(Resource.Error>(e.message.toString())) } + + emit(Resource.Success>(listSongs)) + }.onFailure { e -> + Log.d("Search", "Error: ${e.message}") + emit(Resource.Error>(e.message.toString())) } - }.flowOn(Dispatchers.IO) + } + }.flowOn(Dispatchers.IO) - suspend fun getSearchDataVideo(query: String): Flow>> = - flow { - runCatching { - YouTube.search(query, YouTube.SearchFilter.FILTER_VIDEO).onSuccess { result -> - val listSongs: ArrayList = arrayListOf() - var countinueParam = result.continuation - parseSearchVideo(result).let { list -> - listSongs.addAll(list) - } - var count = 0 - while (count < 2 && countinueParam != null) { - YouTube.searchContinuation(countinueParam).onSuccess { values -> - parseSearchVideo(values).let { list -> - listSongs.addAll(list) - } - count++ - countinueParam = values.continuation - }.onFailure { - Log.e("Continue", "Error: ${it.message}") - countinueParam = null - count++ + suspend fun getSearchDataVideo(query: String): Flow>> = + flow { + runCatching { + YouTube.search(query, YouTube.SearchFilter.FILTER_VIDEO).onSuccess { result -> + val listSongs: ArrayList = arrayListOf() + var countinueParam = result.continuation + parseSearchVideo(result).let { list -> + listSongs.addAll(list) + } + var count = 0 + while (count < 2 && countinueParam != null) { + YouTube.searchContinuation(countinueParam).onSuccess { values -> + parseSearchVideo(values).let { list -> + listSongs.addAll(list) } + count++ + countinueParam = values.continuation + }.onFailure { + Log.e("Continue", "Error: ${it.message}") + countinueParam = null + count++ } + } - emit(Resource.Success>(listSongs)) - }.onFailure { e -> - Log.d("Search", "Error: ${e.message}") - emit(Resource.Error>(e.message.toString())) + emit(Resource.Success>(listSongs)) + }.onFailure { e -> + Log.d("Search", "Error: ${e.message}") + emit(Resource.Error>(e.message.toString())) + } + } + }.flowOn(Dispatchers.IO) + + suspend fun getSearchDataPodcast(query: String): Flow>> = + flow { + runCatching { + YouTube.search(query, YouTube.SearchFilter.FILTER_PODCAST).onSuccess { result -> + println(query) + val listPlaylist: ArrayList = arrayListOf() + var countinueParam = result.continuation + Log.w("Podcast", "result: $result") + parsePodcast(result.listPodcast).let { list -> + listPlaylist.addAll(list) + } + var count = 0 + while (count < 2 && countinueParam != null) { + YouTube.searchContinuation(countinueParam).onSuccess { values -> + parsePodcast(values.listPodcast).let { list -> + listPlaylist.addAll(list) + } + count++ + countinueParam = values.continuation + }.onFailure { + Log.e("Continue", "Error: ${it.message}") + countinueParam = null + count++ + } } + emit(Resource.Success>(listPlaylist)) + }.onFailure { e -> + Log.d("Search", "Error: ${e.message}") + emit(Resource.Error>(e.message.toString())) } - }.flowOn(Dispatchers.IO) + } + }.flowOn(Dispatchers.IO) - suspend fun getSearchDataPodcast(query: String): Flow>> = - flow { - runCatching { - YouTube.search(query, YouTube.SearchFilter.FILTER_PODCAST).onSuccess { result -> - println(query) + suspend fun getSearchDataFeaturedPlaylist(query: String): Flow>> = + flow { + runCatching { + YouTube.search(query, YouTube.SearchFilter.FILTER_FEATURED_PLAYLIST) + .onSuccess { result -> val listPlaylist: ArrayList = arrayListOf() var countinueParam = result.continuation - Log.w("Podcast", "result: $result") - parsePodcast(result.listPodcast).let { list -> + parseSearchPlaylist(result).let { list -> listPlaylist.addAll(list) } var count = 0 while (count < 2 && countinueParam != null) { YouTube.searchContinuation(countinueParam).onSuccess { values -> - parsePodcast(values.listPodcast).let { list -> + parseSearchPlaylist(values).let { list -> listPlaylist.addAll(list) } count++ @@ -1086,107 +1180,76 @@ class MainRepository Log.d("Search", "Error: ${e.message}") emit(Resource.Error>(e.message.toString())) } - } - }.flowOn(Dispatchers.IO) - - suspend fun getSearchDataFeaturedPlaylist(query: String): Flow>> = - flow { - runCatching { - YouTube.search(query, YouTube.SearchFilter.FILTER_FEATURED_PLAYLIST) - .onSuccess { result -> - val listPlaylist: ArrayList = arrayListOf() - var countinueParam = result.continuation - parseSearchPlaylist(result).let { list -> - listPlaylist.addAll(list) - } - var count = 0 - while (count < 2 && countinueParam != null) { - YouTube.searchContinuation(countinueParam).onSuccess { values -> - parseSearchPlaylist(values).let { list -> - listPlaylist.addAll(list) - } - count++ - countinueParam = values.continuation - }.onFailure { - Log.e("Continue", "Error: ${it.message}") - countinueParam = null - count++ - } - } - emit(Resource.Success>(listPlaylist)) - }.onFailure { e -> - Log.d("Search", "Error: ${e.message}") - emit(Resource.Error>(e.message.toString())) - } - } - }.flowOn(Dispatchers.IO) + } + }.flowOn(Dispatchers.IO) - suspend fun getSearchDataArtist(query: String): Flow>> = - flow { - runCatching { - YouTube.search(query, YouTube.SearchFilter.FILTER_ARTIST).onSuccess { result -> - val listArtist: ArrayList = arrayListOf() - var countinueParam = result.continuation - parseSearchArtist(result).let { list -> - listArtist.addAll(list) - } - var count = 0 - while (count < 2 && countinueParam != null) { - YouTube.searchContinuation(countinueParam).onSuccess { values -> - parseSearchArtist(values).let { list -> - listArtist.addAll(list) - } - count++ - countinueParam = values.continuation - }.onFailure { - Log.e("Continue", "Error: ${it.message}") - countinueParam = null - count++ + suspend fun getSearchDataArtist(query: String): Flow>> = + flow { + runCatching { + YouTube.search(query, YouTube.SearchFilter.FILTER_ARTIST).onSuccess { result -> + val listArtist: ArrayList = arrayListOf() + var countinueParam = result.continuation + parseSearchArtist(result).let { list -> + listArtist.addAll(list) + } + var count = 0 + while (count < 2 && countinueParam != null) { + YouTube.searchContinuation(countinueParam).onSuccess { values -> + parseSearchArtist(values).let { list -> + listArtist.addAll(list) } + count++ + countinueParam = values.continuation + }.onFailure { + Log.e("Continue", "Error: ${it.message}") + countinueParam = null + count++ } - emit(Resource.Success>(listArtist)) - }.onFailure { e -> - Log.d("Search", "Error: ${e.message}") - emit(Resource.Error>(e.message.toString())) } + emit(Resource.Success>(listArtist)) + }.onFailure { e -> + Log.d("Search", "Error: ${e.message}") + emit(Resource.Error>(e.message.toString())) } - }.flowOn(Dispatchers.IO) + } + }.flowOn(Dispatchers.IO) - suspend fun getSearchDataAlbum(query: String): Flow>> = - flow { - runCatching { - YouTube.search(query, YouTube.SearchFilter.FILTER_ALBUM).onSuccess { result -> - val listAlbum: ArrayList = arrayListOf() - var countinueParam = result.continuation - parseSearchAlbum(result).let { list -> - listAlbum.addAll(list) - } - var count = 0 - while (count < 2 && countinueParam != null) { - YouTube.searchContinuation(countinueParam).onSuccess { values -> - parseSearchAlbum(values).let { list -> - listAlbum.addAll(list) - } - count++ - countinueParam = values.continuation - }.onFailure { - Log.e("Continue", "Error: ${it.message}") - countinueParam = null - count++ + suspend fun getSearchDataAlbum(query: String): Flow>> = + flow { + runCatching { + YouTube.search(query, YouTube.SearchFilter.FILTER_ALBUM).onSuccess { result -> + val listAlbum: ArrayList = arrayListOf() + var countinueParam = result.continuation + parseSearchAlbum(result).let { list -> + listAlbum.addAll(list) + } + var count = 0 + while (count < 2 && countinueParam != null) { + YouTube.searchContinuation(countinueParam).onSuccess { values -> + parseSearchAlbum(values).let { list -> + listAlbum.addAll(list) } + count++ + countinueParam = values.continuation + }.onFailure { + Log.e("Continue", "Error: ${it.message}") + countinueParam = null + count++ } - emit(Resource.Success>(listAlbum)) - }.onFailure { e -> - Log.d("Search", "Error: ${e.message}") - emit(Resource.Error>(e.message.toString())) } + emit(Resource.Success>(listAlbum)) + }.onFailure { e -> + Log.d("Search", "Error: ${e.message}") + emit(Resource.Error>(e.message.toString())) } - }.flowOn(Dispatchers.IO) + } + }.flowOn(Dispatchers.IO) - suspend fun getSearchDataPlaylist(query: String): Flow>> = - flow { - runCatching { - YouTube.search(query, YouTube.SearchFilter.FILTER_COMMUNITY_PLAYLIST).onSuccess { result -> + suspend fun getSearchDataPlaylist(query: String): Flow>> = + flow { + runCatching { + YouTube.search(query, YouTube.SearchFilter.FILTER_COMMUNITY_PLAYLIST) + .onSuccess { result -> val listPlaylist: ArrayList = arrayListOf() var countinueParam = result.continuation parseSearchPlaylist(result).let { list -> @@ -1211,47 +1274,47 @@ class MainRepository Log.d("Search", "Error: ${e.message}") emit(Resource.Error>(e.message.toString())) } - } - }.flowOn(Dispatchers.IO) + } + }.flowOn(Dispatchers.IO) - suspend fun getSuggestQuery(query: String): Flow> = - flow { - runCatching { + suspend fun getSuggestQuery(query: String): Flow> = + flow { + runCatching { // YouTube.getSuggestQuery(query).onSuccess { // emit(Resource.Success>(it)) // }.onFailure { e -> // Log.d("Suggest", "Error: ${e.message}") // emit(Resource.Error>(e.message.toString())) // } - YouTube.getYTMusicSearchSuggestions(query).onSuccess { - emit(Resource.Success(it)) - }.onFailure { e -> - Log.d("Suggest", "Error: ${e.message}") - emit(Resource.Error(e.message.toString())) - } + YouTube.getYTMusicSearchSuggestions(query).onSuccess { + emit(Resource.Success(it)) + }.onFailure { e -> + Log.d("Suggest", "Error: ${e.message}") + emit(Resource.Error(e.message.toString())) } - }.flowOn(Dispatchers.IO) + } + }.flowOn(Dispatchers.IO) - suspend fun getRelatedData(videoId: String): Flow>> = - flow { - runCatching { - Queue.setContinuation(null) - YouTube.next(WatchEndpoint(playlistId = "RDAMVM$videoId")) - .onSuccess { next -> - val data: ArrayList = arrayListOf() - data.addAll(next.items) - val nextContinuation = next.continuation - if (nextContinuation != null) { - Queue.setContinuation(Pair("RDAMVM$videoId", nextContinuation)) - } else { - Queue.setContinuation(null) - } - emit(Resource.Success>(data.toListTrack())) - }.onFailure { exception -> - exception.printStackTrace() + suspend fun getRelatedData(videoId: String): Flow>> = + flow { + runCatching { + Queue.setContinuation(null) + YouTube.next(WatchEndpoint(playlistId = "RDAMVM$videoId")) + .onSuccess { next -> + val data: ArrayList = arrayListOf() + data.addAll(next.items) + val nextContinuation = next.continuation + if (nextContinuation != null) { + Queue.setContinuation(Pair("RDAMVM$videoId", nextContinuation)) + } else { Queue.setContinuation(null) - emit(Resource.Error>(exception.message.toString())) } + emit(Resource.Success>(data.toListTrack())) + }.onFailure { exception -> + exception.printStackTrace() + Queue.setContinuation(null) + emit(Resource.Error>(exception.message.toString())) + } // YouTube.nextCustom(videoId).onSuccess { result -> // val listSongs: ArrayList = arrayListOf() // val data = @@ -1273,124 +1336,31 @@ class MainRepository // Log.d("Related", "Error: ${e.message}") // emit(Resource.Error>(e.message.toString())) // } - } - }.flowOn(Dispatchers.IO) - - suspend fun getYouTubeCaption(videoId: String): Flow> = - flow { - runCatching { - YouTube.getYouTubeCaption(videoId).onSuccess { lyrics -> - Log.w("Lyrics", "lyrics: ${lyrics.toLyrics()}") - emit(Resource.Success(lyrics.toLyrics())) - }.onFailure { e -> - Log.d("Lyrics", "Error: ${e.message}") - emit(Resource.Error(e.message.toString())) - } - } } + }.flowOn(Dispatchers.IO) - suspend fun getCanvas( - videoId: String, - duration: Int, - ): Flow = - flow { - runCatching { - getSongById(videoId).first().let { song -> - val q = - "${song?.title} ${song?.artistName?.firstOrNull() ?: ""}".replace( - Regex("\\((feat\\.|ft.|cùng với|con|mukana|com|avec|合作音乐人: ) "), - " ", - ).replace( - Regex("( và | & | и | e | und |, |和| dan)"), - " ", - ).replace(" ", " ").replace(Regex("([()])"), "").replace(".", " ") - .replace(" ", " ") - var spotifyPersonalToken = "" - if (dataStoreManager.spotifyPersonalToken.first() - .isNotEmpty() && dataStoreManager.spotifyPersonalTokenExpires.first() > System.currentTimeMillis() && dataStoreManager.spotifyPersonalTokenExpires.first() != 0L - ) { - spotifyPersonalToken = dataStoreManager.spotifyPersonalToken.first() - Log.d("Lyrics", "spotifyPersonalToken: $spotifyPersonalToken") - } else if (dataStoreManager.spdc.first().isNotEmpty()) { - YouTube.getPersonalToken(dataStoreManager.spdc.first()).onSuccess { - spotifyPersonalToken = it.accessToken - dataStoreManager.setSpotifyPersonalToken(spotifyPersonalToken) - dataStoreManager.setSpotifyPersonalTokenExpires(it.accessTokenExpirationTimestampMs) - Log.d("Lyrics", "spotifyPersonalToken: $spotifyPersonalToken") - }.onFailure { - it.printStackTrace() - emit(null) - } - } - if (spotifyPersonalToken.isNotEmpty()) { - var clientToken = dataStoreManager.spotifyClientToken.first() - Log.d("Lyrics", "clientToken: $clientToken") - YouTube.searchSpotifyTrack(q, clientToken).onSuccess { searchResponse -> - val track = - if (duration != 0) { - searchResponse.tracks.items.find { abs(((it.duration_ms / 1000) - duration)) < 1 } - ?: searchResponse.tracks.items.firstOrNull() - } else { - searchResponse.tracks.items.firstOrNull() - } - Log.d("Lyrics", "track: $track") - if (track != null) { - YouTube.getSpotifyCanvas(track.id, spotifyPersonalToken).onSuccess { - Log.w("Spotify Canvas", "canvas: $it") - emit(it) - }.onFailure { - it.printStackTrace() - emit(null) - } - } else { - emit(null) - } - }.onFailure { throwable -> - throwable.printStackTrace() - YouTube.getClientToken().onSuccess { tokenResponse -> - clientToken = tokenResponse.accessToken - Log.w("Lyrics", "clientToken: $clientToken") - dataStoreManager.setSpotifyClientToken(clientToken) - YouTube.searchSpotifyTrack(q, clientToken).onSuccess { searchResponse -> - val track = - if (duration != 0) { - searchResponse.tracks.items.find { abs(((it.duration_ms / 1000) - duration)) < 1 } - ?: searchResponse.tracks.items.firstOrNull() - } else { - searchResponse.tracks.items.firstOrNull() - } - Log.d("Lyrics", "track: $track") - if (track != null) { - YouTube.getSpotifyCanvas(track.id, spotifyPersonalToken) - .onSuccess { - Log.w("Spotify Canvas", "canvas: $it") - emit(it) - }.onFailure { - it.printStackTrace() - emit(null) - } - } else { - emit(null) - } - } - }.onFailure { - it.printStackTrace() - emit(null) - } - } - } - } + suspend fun getYouTubeCaption(videoId: String): Flow> = + flow { + runCatching { + YouTube.getYouTubeCaption(videoId).onSuccess { lyrics -> + Log.w("Lyrics", "lyrics: ${lyrics.toLyrics()}") + emit(Resource.Success(lyrics.toLyrics())) + }.onFailure { e -> + Log.d("Lyrics", "Error: ${e.message}") + emit(Resource.Error(e.message.toString())) } - }.flowOn(Dispatchers.IO) + } + } - suspend fun getSpotifyLyrics( - query: String, - duration: Int?, - ): Flow> = - flow { - runCatching { + suspend fun getCanvas( + videoId: String, + duration: Int, + ): Flow = + flow { + runCatching { + getSongById(videoId).first().let { song -> val q = - query.replace( + "${song?.title} ${song?.artistName?.firstOrNull() ?: ""}".replace( Regex("\\((feat\\.|ft.|cùng với|con|mukana|com|avec|合作音乐人: ) "), " ", ).replace( @@ -1398,7 +1368,6 @@ class MainRepository " ", ).replace(" ", " ").replace(Regex("([()])"), "").replace(".", " ") .replace(" ", " ") - Log.d("Lyrics", "query: $q") var spotifyPersonalToken = "" if (dataStoreManager.spotifyPersonalToken.first() .isNotEmpty() && dataStoreManager.spotifyPersonalTokenExpires.first() > System.currentTimeMillis() && dataStoreManager.spotifyPersonalTokenExpires.first() != 0L @@ -1413,7 +1382,7 @@ class MainRepository Log.d("Lyrics", "spotifyPersonalToken: $spotifyPersonalToken") }.onFailure { it.printStackTrace() - emit(Resource.Error("Not found")) + emit(null) } } if (spotifyPersonalToken.isNotEmpty()) { @@ -1421,7 +1390,7 @@ class MainRepository Log.d("Lyrics", "clientToken: $clientToken") YouTube.searchSpotifyTrack(q, clientToken).onSuccess { searchResponse -> val track = - if (duration != null && duration != 0) { + if (duration != 0) { searchResponse.tracks.items.find { abs(((it.duration_ms / 1000) - duration)) < 1 } ?: searchResponse.tracks.items.firstOrNull() } else { @@ -1429,79 +1398,176 @@ class MainRepository } Log.d("Lyrics", "track: $track") if (track != null) { - YouTube.getSpotifyLyrics(track.id, spotifyPersonalToken).onSuccess { - emit(Resource.Success(it.toLyrics())) + YouTube.getSpotifyCanvas(track.id, spotifyPersonalToken).onSuccess { + Log.w("Spotify Canvas", "canvas: $it") + emit(it) }.onFailure { it.printStackTrace() - emit(Resource.Error("Not found")) + emit(null) } } else { + emit(null) + } + }.onFailure { throwable -> + throwable.printStackTrace() + YouTube.getClientToken().onSuccess { tokenResponse -> + clientToken = tokenResponse.accessToken + Log.w("Lyrics", "clientToken: $clientToken") + dataStoreManager.setSpotifyClientToken(clientToken) + YouTube.searchSpotifyTrack(q, clientToken) + .onSuccess { searchResponse -> + val track = + if (duration != 0) { + searchResponse.tracks.items.find { abs(((it.duration_ms / 1000) - duration)) < 1 } + ?: searchResponse.tracks.items.firstOrNull() + } else { + searchResponse.tracks.items.firstOrNull() + } + Log.d("Lyrics", "track: $track") + if (track != null) { + YouTube.getSpotifyCanvas(track.id, spotifyPersonalToken) + .onSuccess { + Log.w("Spotify Canvas", "canvas: $it") + emit(it) + }.onFailure { + it.printStackTrace() + emit(null) + } + } else { + emit(null) + } + } + }.onFailure { + it.printStackTrace() + emit(null) + } + } + } + } + } + }.flowOn(Dispatchers.IO) + + suspend fun getSpotifyLyrics( + query: String, + duration: Int?, + ): Flow> = + flow { + runCatching { + val q = + query.replace( + Regex("\\((feat\\.|ft.|cùng với|con|mukana|com|avec|合作音乐人: ) "), + " ", + ).replace( + Regex("( và | & | и | e | und |, |和| dan)"), + " ", + ).replace(" ", " ").replace(Regex("([()])"), "").replace(".", " ") + .replace(" ", " ") + Log.d("Lyrics", "query: $q") + var spotifyPersonalToken = "" + if (dataStoreManager.spotifyPersonalToken.first() + .isNotEmpty() && dataStoreManager.spotifyPersonalTokenExpires.first() > System.currentTimeMillis() && dataStoreManager.spotifyPersonalTokenExpires.first() != 0L + ) { + spotifyPersonalToken = dataStoreManager.spotifyPersonalToken.first() + Log.d("Lyrics", "spotifyPersonalToken: $spotifyPersonalToken") + } else if (dataStoreManager.spdc.first().isNotEmpty()) { + YouTube.getPersonalToken(dataStoreManager.spdc.first()).onSuccess { + spotifyPersonalToken = it.accessToken + dataStoreManager.setSpotifyPersonalToken(spotifyPersonalToken) + dataStoreManager.setSpotifyPersonalTokenExpires(it.accessTokenExpirationTimestampMs) + Log.d("Lyrics", "spotifyPersonalToken: $spotifyPersonalToken") + }.onFailure { + it.printStackTrace() + emit(Resource.Error("Not found")) + } + } + if (spotifyPersonalToken.isNotEmpty()) { + var clientToken = dataStoreManager.spotifyClientToken.first() + Log.d("Lyrics", "clientToken: $clientToken") + YouTube.searchSpotifyTrack(q, clientToken).onSuccess { searchResponse -> + val track = + if (duration != null && duration != 0) { + searchResponse.tracks.items.find { abs(((it.duration_ms / 1000) - duration)) < 1 } + ?: searchResponse.tracks.items.firstOrNull() + } else { + searchResponse.tracks.items.firstOrNull() + } + Log.d("Lyrics", "track: $track") + if (track != null) { + YouTube.getSpotifyLyrics(track.id, spotifyPersonalToken).onSuccess { + emit(Resource.Success(it.toLyrics())) + }.onFailure { + it.printStackTrace() emit(Resource.Error("Not found")) } - }.onFailure { throwable -> - throwable.printStackTrace() - YouTube.getClientToken().onSuccess { - clientToken = it.accessToken - Log.w("Lyrics", "clientToken: $clientToken") - dataStoreManager.setSpotifyClientToken(clientToken) - YouTube.searchSpotifyTrack(q, clientToken).onSuccess { searchResponse -> - val track = - if (duration != null && duration != 0) { - searchResponse.tracks.items.find { abs(((it.duration_ms / 1000) - duration)) < 1 } - ?: searchResponse.tracks.items.firstOrNull() - } else { - searchResponse.tracks.items.firstOrNull() - } - Log.d("Lyrics", "track: $track") - if (track != null) { - YouTube.getSpotifyLyrics(track.id, spotifyPersonalToken).onSuccess { + } else { + emit(Resource.Error("Not found")) + } + }.onFailure { throwable -> + throwable.printStackTrace() + YouTube.getClientToken().onSuccess { + clientToken = it.accessToken + Log.w("Lyrics", "clientToken: $clientToken") + dataStoreManager.setSpotifyClientToken(clientToken) + YouTube.searchSpotifyTrack(q, clientToken).onSuccess { searchResponse -> + val track = + if (duration != null && duration != 0) { + searchResponse.tracks.items.find { abs(((it.duration_ms / 1000) - duration)) < 1 } + ?: searchResponse.tracks.items.firstOrNull() + } else { + searchResponse.tracks.items.firstOrNull() + } + Log.d("Lyrics", "track: $track") + if (track != null) { + YouTube.getSpotifyLyrics(track.id, spotifyPersonalToken) + .onSuccess { emit(Resource.Success(it.toLyrics())) }.onFailure { it.printStackTrace() emit(Resource.Error("Not found")) } - } else { - emit(Resource.Error("Not found")) - } + } else { + emit(Resource.Error("Not found")) } - }.onFailure { - it.printStackTrace() - emit(Resource.Error("Not found")) } + }.onFailure { + it.printStackTrace() + emit(Resource.Error("Not found")) } } } } + } - suspend fun getLyricsData( - query: String, - durationInt: Int? = null, - ): Flow>> = - flow { - runCatching { + suspend fun getLyricsData( + query: String, + durationInt: Int? = null, + ): Flow>> = + flow { + runCatching { // val q = query.replace(Regex("\\([^)]*?(feat.|ft.|cùng với|con)[^)]*?\\)"), "") // .replace(" ", " ") - val q = - query.replace( - Regex("\\((feat\\.|ft.|cùng với|con|mukana|com|avec|合作音乐人: ) "), - " ", - ).replace( - Regex("( và | & | и | e | und |, |和| dan)"), - " ", - ).replace(" ", " ").replace(Regex("([()])"), "").replace(".", " ") - Log.d("Lyrics", "query: $q") - var musixMatchUserToken = YouTube.musixmatchUserToken - if (musixMatchUserToken == null) { - YouTube.getMusixmatchUserToken().onSuccess { usertoken -> - YouTube.musixmatchUserToken = usertoken.message.body.user_token - musixMatchUserToken = usertoken.message.body.user_token - } - .onFailure { throwable -> - throwable.printStackTrace() - emit(Pair("", Resource.Error("Not found"))) - } + val q = + query.replace( + Regex("\\((feat\\.|ft.|cùng với|con|mukana|com|avec|合作音乐人: ) "), + " ", + ).replace( + Regex("( và | & | и | e | und |, |和| dan)"), + " ", + ).replace(" ", " ").replace(Regex("([()])"), "").replace(".", " ") + Log.d("Lyrics", "query: $q") + var musixMatchUserToken = YouTube.musixmatchUserToken + if (musixMatchUserToken == null) { + YouTube.getMusixmatchUserToken().onSuccess { usertoken -> + YouTube.musixmatchUserToken = usertoken.message.body.user_token + musixMatchUserToken = usertoken.message.body.user_token } - YouTube.searchMusixmatchTrackId(q, musixMatchUserToken!!).onSuccess { searchResult -> + .onFailure { throwable -> + throwable.printStackTrace() + emit(Pair("", Resource.Error("Not found"))) + } + } + YouTube.searchMusixmatchTrackId(q, musixMatchUserToken!!) + .onSuccess { searchResult -> Log.d("Lyrics", "searchResult: $searchResult") if (searchResult.message.body.track_list.isNotEmpty()) { val list = arrayListOf() @@ -1526,7 +1592,9 @@ class MainRepository } if (id == "") { if (list.get(bestMatchingIndex).contains( - searchResult.message.body.track_list.get(bestMatchingIndex).track.track_name, + searchResult.message.body.track_list.get( + bestMatchingIndex + ).track.track_name, ) && query.contains( searchResult.message.body.track_list.get( @@ -1537,24 +1605,38 @@ class MainRepository Log.w( "Lyrics", "item: ${ - searchResult.message.body.track_list.get(bestMatchingIndex).track.track_name + searchResult.message.body.track_list.get( + bestMatchingIndex + ).track.track_name }", ) - id += searchResult.message.body.track_list.get(bestMatchingIndex).track.track_id.toString() + id += searchResult.message.body.track_list.get( + bestMatchingIndex + ).track.track_id.toString() track = - searchResult.message.body.track_list.get(bestMatchingIndex).track + searchResult.message.body.track_list.get( + bestMatchingIndex + ).track } } } else { if (list.get(bestMatchingIndex) - .contains(searchResult.message.body.track_list.get(bestMatchingIndex).track.track_name) && + .contains( + searchResult.message.body.track_list.get( + bestMatchingIndex + ).track.track_name + ) && query.contains( searchResult.message.body.track_list.get(bestMatchingIndex).track.track_name, ) ) { Log.w( "Lyrics", - "item: ${searchResult.message.body.track_list.get(bestMatchingIndex).track.track_name}", + "item: ${ + searchResult.message.body.track_list.get( + bestMatchingIndex + ).track.track_name + }", ) id += searchResult.message.body.track_list.get(bestMatchingIndex).track.track_id.toString() track = @@ -1581,17 +1663,18 @@ class MainRepository // it.printStackTrace() // emit(Pair(id, Resource.Error("Not found"))) // } - YouTube.getMusixmatchLyricsByQ(track, musixMatchUserToken!!).onSuccess { - if (it != null) { - emit(Pair(id, Resource.Success(it.toLyrics()))) - } else { - Log.w("Lyrics", "Error: Lỗi getLyrics $it") + YouTube.getMusixmatchLyricsByQ(track, musixMatchUserToken!!) + .onSuccess { + if (it != null) { + emit(Pair(id, Resource.Success(it.toLyrics()))) + } else { + Log.w("Lyrics", "Error: Lỗi getLyrics $it") + emit(Pair(id, Resource.Error("Not found"))) + } + }.onFailure { throwable -> + throwable.printStackTrace() emit(Pair(id, Resource.Error("Not found"))) } - }.onFailure { throwable -> - throwable.printStackTrace() - emit(Pair(id, Resource.Error("Not found"))) - } } // bestMatchingIndex(q, list).let { index -> // Log.w("Lyrics", "item: ${searchResult.message.body.track_list.get(index).track.track_name}") @@ -1606,10 +1689,10 @@ class MainRepository emit(Pair("", Resource.Error("Not found"))) } } - .onFailure { throwable -> - throwable.printStackTrace() - emit(Pair("", Resource.Error("Not found"))) - } + .onFailure { throwable -> + throwable.printStackTrace() + emit(Pair("", Resource.Error("Not found"))) + } // YouTube.authentication().onSuccess { token -> // if (token.accessToken != null) { @@ -1827,142 +1910,142 @@ class MainRepository // Log.d("SongId", "Error: ${it.message}") // emit(Resource.Error("Not found")) // } - } - }.flowOn(Dispatchers.IO) + } + }.flowOn(Dispatchers.IO) - suspend fun getTranslateLyrics(id: String): Flow = - flow { - runCatching { - YouTube.musixmatchUserToken?.let { - YouTube.getMusixmatchTranslateLyrics( - id, - it, - dataStoreManager.translationLanguage.first(), - ) - .onSuccess { lyrics -> - emit(lyrics) - } - .onFailure { - it.printStackTrace() - emit(null) - } - } + suspend fun getTranslateLyrics(id: String): Flow = + flow { + runCatching { + YouTube.musixmatchUserToken?.let { + YouTube.getMusixmatchTranslateLyrics( + id, + it, + dataStoreManager.translationLanguage.first(), + ) + .onSuccess { lyrics -> + emit(lyrics) + } + .onFailure { + it.printStackTrace() + emit(null) + } } - }.flowOn(Dispatchers.IO) + } + }.flowOn(Dispatchers.IO) - suspend fun getSongInfo(videoId: String): Flow = - flow { - runCatching { - YouTube.getSongInfo(videoId).onSuccess { songInfo -> - val song = - SongInfoEntity( - videoId = songInfo.videoId, - author = songInfo.author, - authorId = songInfo.authorId, - authorThumbnail = songInfo.authorThumbnail, - description = songInfo.description, - uploadDate = songInfo.uploadDate, - subscribers = songInfo.subscribers, - viewCount = songInfo.viewCount, - like = songInfo.like, - dislike = songInfo.dislike, - ) - emit(song) - insertSongInfo( - song, + suspend fun getSongInfo(videoId: String): Flow = + flow { + runCatching { + YouTube.getSongInfo(videoId).onSuccess { songInfo -> + val song = + SongInfoEntity( + videoId = songInfo.videoId, + author = songInfo.author, + authorId = songInfo.authorId, + authorThumbnail = songInfo.authorThumbnail, + description = songInfo.description, + uploadDate = songInfo.uploadDate, + subscribers = songInfo.subscribers, + viewCount = songInfo.viewCount, + like = songInfo.like, + dislike = songInfo.dislike, ) - }.onFailure { - it.printStackTrace() - emit(getSongInfoEntiy(videoId).firstOrNull()) - } + emit(song) + insertSongInfo( + song, + ) + }.onFailure { + it.printStackTrace() + emit(getSongInfoEntiy(videoId).firstOrNull()) } - }.flowOn(Dispatchers.IO) - - suspend fun getStream( - videoId: String, - itag: Int, - ): Flow = - flow { - YouTube.player(videoId).onSuccess { data -> - val acceptToPlayVideo = - runBlocking { dataStoreManager.watchVideoInsteadOfPlayingAudio.first() == DataStoreManager.TRUE } - val videoItag = - VIDEO_QUALITY.itags.getOrNull(VIDEO_QUALITY.items.indexOf(dataStoreManager.videoQuality.first())) - ?: 22 - val response = data.second - if (data.third == MediaType.Song) { - Log.w( - "Stream", - "response: is SONG", - ) - } else { - Log.w("Stream", "response: is VIDEO") - } - Log.w("Stream: ", data.toString()) - var format = - if (acceptToPlayVideo) { - if (data.third == MediaType.Song) { - if (response.streamingData?.adaptiveFormats?.find { it.itag == 141 } != null) { - response.streamingData?.adaptiveFormats?.find { it.itag == 141 } - } else { - response.streamingData?.adaptiveFormats?.find { it.itag == itag } - } - } else { - response.streamingData?.formats?.find { it.itag == videoItag } - } - } else { + } + }.flowOn(Dispatchers.IO) + + suspend fun getStream( + videoId: String, + itag: Int, + ): Flow = + flow { + YouTube.player(videoId).onSuccess { data -> + val acceptToPlayVideo = + runBlocking { dataStoreManager.watchVideoInsteadOfPlayingAudio.first() == DataStoreManager.TRUE } + val videoItag = + VIDEO_QUALITY.itags.getOrNull(VIDEO_QUALITY.items.indexOf(dataStoreManager.videoQuality.first())) + ?: 22 + val response = data.second + if (data.third == MediaType.Song) { + Log.w( + "Stream", + "response: is SONG", + ) + } else { + Log.w("Stream", "response: is VIDEO") + } + Log.w("Stream: ", data.toString()) + var format = + if (acceptToPlayVideo) { + if (data.third == MediaType.Song) { if (response.streamingData?.adaptiveFormats?.find { it.itag == 141 } != null) { response.streamingData?.adaptiveFormats?.find { it.itag == 141 } } else { response.streamingData?.adaptiveFormats?.find { it.itag == itag } } + } else { + response.streamingData?.formats?.find { it.itag == videoItag } } - if (format == null) { - format = response.streamingData?.adaptiveFormats?.lastOrNull() - } - Log.w("Stream", "format: $format") - runBlocking { - insertNewFormat( - NewFormatEntity( - videoId = videoId, - itag = format?.itag ?: itag, - mimeType = - Regex("""([^;]+);\s*codecs=["']([^"']+)["']""").find( - format?.mimeType ?: "", - )?.groupValues?.getOrNull(1) ?: format?.mimeType ?: "", - codecs = - Regex("""([^;]+);\s*codecs=["']([^"']+)["']""").find( - format?.mimeType ?: "", - )?.groupValues?.getOrNull(2) ?: format?.mimeType ?: "", - bitrate = format?.bitrate, - sampleRate = format?.audioSampleRate, - contentLength = format?.contentLength, - loudnessDb = response.playerConfig?.audioConfig?.loudnessDb?.toFloat(), - lengthSeconds = response.videoDetails?.lengthSeconds?.toInt(), - playbackTrackingVideostatsPlaybackUrl = - response.playbackTracking?.videostatsPlaybackUrl?.baseUrl?.replace( - "https://s.youtube.com", - "https://music.youtube.com", - ), - playbackTrackingAtrUrl = - response.playbackTracking?.atrUrl?.baseUrl?.replace( - "https://s.youtube.com", - "https://music.youtube.com", - ), - playbackTrackingVideostatsWatchtimeUrl = - response.playbackTracking?.videostatsWatchtimeUrl?.baseUrl?.replace( - "https://s.youtube.com", - "https://music.youtube.com", - ), - cpn = data.first, - ), - ) - } - if (data.first != null) { - emit(format?.url?.plus("&cpn=${data.first}")) } else { - emit(format?.url) + if (response.streamingData?.adaptiveFormats?.find { it.itag == 141 } != null) { + response.streamingData?.adaptiveFormats?.find { it.itag == 141 } + } else { + response.streamingData?.adaptiveFormats?.find { it.itag == itag } + } } + if (format == null) { + format = response.streamingData?.adaptiveFormats?.lastOrNull() + } + Log.w("Stream", "format: $format") + runBlocking { + insertNewFormat( + NewFormatEntity( + videoId = videoId, + itag = format?.itag ?: itag, + mimeType = + Regex("""([^;]+);\s*codecs=["']([^"']+)["']""").find( + format?.mimeType ?: "", + )?.groupValues?.getOrNull(1) ?: format?.mimeType ?: "", + codecs = + Regex("""([^;]+);\s*codecs=["']([^"']+)["']""").find( + format?.mimeType ?: "", + )?.groupValues?.getOrNull(2) ?: format?.mimeType ?: "", + bitrate = format?.bitrate, + sampleRate = format?.audioSampleRate, + contentLength = format?.contentLength, + loudnessDb = response.playerConfig?.audioConfig?.loudnessDb?.toFloat(), + lengthSeconds = response.videoDetails?.lengthSeconds?.toInt(), + playbackTrackingVideostatsPlaybackUrl = + response.playbackTracking?.videostatsPlaybackUrl?.baseUrl?.replace( + "https://s.youtube.com", + "https://music.youtube.com", + ), + playbackTrackingAtrUrl = + response.playbackTracking?.atrUrl?.baseUrl?.replace( + "https://s.youtube.com", + "https://music.youtube.com", + ), + playbackTrackingVideostatsWatchtimeUrl = + response.playbackTracking?.videostatsWatchtimeUrl?.baseUrl?.replace( + "https://s.youtube.com", + "https://music.youtube.com", + ), + cpn = data.first, + ), + ) + } + if (data.first != null) { + emit(format?.url?.plus("&cpn=${data.first}")) + } else { + emit(format?.url) + } // insertFormat( // FormatEntity( // videoId = videoId, @@ -1983,307 +2066,330 @@ class MainRepository // lengthSeconds = response.videoDetails?.lengthSeconds?.toInt(), // ) // ) - }.onFailure { - it.printStackTrace() - Log.e("Stream", "Error: ${it.message}") + }.onFailure { + it.printStackTrace() + Log.e("Stream", "Error: ${it.message}") + emit(null) + } + }.flowOn(Dispatchers.IO) + + suspend fun getLibraryPlaylist(): Flow?> = + flow { + YouTube.getLibraryPlaylists().onSuccess { data -> + val input = + data.contents?.singleColumnBrowseResultsRenderer?.tabs?.get( + 0, + )?.tabRenderer?.content?.sectionListRenderer?.contents?.get(0)?.gridRenderer?.items + ?: null + if (input != null) { + Log.w("Library", "input: ${input.size}") + val list = parseLibraryPlaylist(input) + emit(list) + } else { emit(null) } - }.flowOn(Dispatchers.IO) - - suspend fun getLibraryPlaylist(): Flow?> = - flow { - YouTube.getLibraryPlaylists().onSuccess { data -> - val input = - data.contents?.singleColumnBrowseResultsRenderer?.tabs?.get( - 0, - )?.tabRenderer?.content?.sectionListRenderer?.contents?.get(0)?.gridRenderer?.items ?: null - if (input != null) { - Log.w("Library", "input: ${input.size}") - val list = parseLibraryPlaylist(input) - emit(list) - } else { - emit(null) - } - } - .onFailure { e -> - Log.e("Library", "Error: ${e.message}") - e.printStackTrace() - emit(null) - } - }.flowOn(Dispatchers.IO) - - suspend fun initPlayback( - playback: String, - atr: String, - watchTime: String, - cpn: String, - playlistId: String?, - ): Flow> = - flow { - YouTube.initPlayback(playback, atr, watchTime, cpn, playlistId).onSuccess { response -> - emit(response) - }.onFailure { - Log.e("InitPlayback", "Error: ${it.message}") - emit(Pair(0, 0f)) - } - }.flowOn(Dispatchers.IO) - - suspend fun getSkipSegments(videoId: String): Flow?> = - flow { - YouTube.getSkipSegments(videoId).onSuccess { - emit(it) - }.onFailure { + } + .onFailure { e -> + Log.e("Library", "Error: ${e.message}") + e.printStackTrace() emit(null) } - }.flowOn(Dispatchers.IO) + }.flowOn(Dispatchers.IO) + + suspend fun initPlayback( + playback: String, + atr: String, + watchTime: String, + cpn: String, + playlistId: String?, + ): Flow> = + flow { + YouTube.initPlayback(playback, atr, watchTime, cpn, playlistId).onSuccess { response -> + emit(response) + }.onFailure { + Log.e("InitPlayback", "Error: ${it.message}") + emit(Pair(0, 0f)) + } + }.flowOn(Dispatchers.IO) + + suspend fun getSkipSegments(videoId: String): Flow?> = + flow { + YouTube.getSkipSegments(videoId).onSuccess { + emit(it) + }.onFailure { + emit(null) + } + }.flowOn(Dispatchers.IO) + + fun getFullMetadata(videoId: String): Flow = + flow { + Log.w("getFullMetadata", "videoId: $videoId") + YouTube.getFullMetadata(videoId).onSuccess { + emit(it) + }.onFailure { + Log.e("getFullMetadata", "Error: ${it.message}") + emit(null) + } + }.flowOn(Dispatchers.IO) - fun getFullMetadata(videoId: String): Flow = - flow { - Log.w("getFullMetadata", "videoId: $videoId") - YouTube.getFullMetadata(videoId).onSuccess { - emit(it) - }.onFailure { - Log.e("getFullMetadata", "Error: ${it.message}") + fun checkForUpdate(): Flow = + flow { + YouTube.checkForUpdate().onSuccess { + emit(it) + } + .onFailure { emit(null) } - }.flowOn(Dispatchers.IO) + } - fun checkForUpdate(): Flow = - flow { - YouTube.checkForUpdate().onSuccess { - emit(it) + suspend fun getYouTubeSetVideoId(youtubePlaylistId: String): Flow?> = + flow { + runCatching { + var id = "" + if (!youtubePlaylistId.startsWith("VL")) { + id += "VL$youtubePlaylistId" + } else { + id += youtubePlaylistId } - .onFailure { - emit(null) - } - } - - suspend fun getYouTubeSetVideoId(youtubePlaylistId: String): Flow?> = - flow { - runCatching { - var id = "" - if (!youtubePlaylistId.startsWith("VL")) { - id += "VL$youtubePlaylistId" - } else { - id += youtubePlaylistId + Log.d("Repository", "playlist id: $id") + YouTube.customQuery(browseId = id, setLogin = true).onSuccess { result -> + val listContent: ArrayList = arrayListOf() + val data: List? = + result.contents?.singleColumnBrowseResultsRenderer?.tabs?.get( + 0, + )?.tabRenderer?.content?.sectionListRenderer?.contents?.get(0)?.musicPlaylistShelfRenderer?.contents + if (data != null) { + Log.d("Data", "data: $data") + Log.d("Data", "data size: ${data.size}") + listContent.addAll(data) } - Log.d("Repository", "playlist id: $id") - YouTube.customQuery(browseId = id, setLogin = true).onSuccess { result -> - val listContent: ArrayList = arrayListOf() - val data: List? = - result.contents?.singleColumnBrowseResultsRenderer?.tabs?.get( - 0, - )?.tabRenderer?.content?.sectionListRenderer?.contents?.get(0)?.musicPlaylistShelfRenderer?.contents - if (data != null) { - Log.d("Data", "data: $data") - Log.d("Data", "data size: ${data.size}") - listContent.addAll(data) - } - var continueParam = - result.contents?.singleColumnBrowseResultsRenderer?.tabs?.get( - 0, - )?.tabRenderer?.content?.sectionListRenderer?.contents?.get( - 0, - )?.musicPlaylistShelfRenderer?.continuations?.get(0)?.nextContinuationData?.continuation - var count = 0 - Log.d("Repository", "playlist data: ${listContent.size}") - Log.d("Repository", "continueParam: $continueParam") - while (continueParam != null) { - YouTube.customQuery(browseId = "", continuation = continueParam, setLogin = true).onSuccess { values -> - Log.d("Continue", "continue: $continueParam") - val dataMore: List? = values.continuationContents?.musicPlaylistShelfContinuation?.contents - if (dataMore != null) { - listContent.addAll(dataMore) - } - continueParam = - values.continuationContents?.musicPlaylistShelfContinuation?.continuations?.get( - 0, - )?.nextContinuationData?.continuation - count++ - }.onFailure { - Log.e("Continue", "Error: ${it.message}") - continueParam = null - count++ + var continueParam = + result.contents?.singleColumnBrowseResultsRenderer?.tabs?.get( + 0, + )?.tabRenderer?.content?.sectionListRenderer?.contents?.get( + 0, + )?.musicPlaylistShelfRenderer?.continuations?.get(0)?.nextContinuationData?.continuation + var count = 0 + Log.d("Repository", "playlist data: ${listContent.size}") + Log.d("Repository", "continueParam: $continueParam") + while (continueParam != null) { + YouTube.customQuery( + browseId = "", + continuation = continueParam, + setLogin = true + ).onSuccess { values -> + Log.d("Continue", "continue: $continueParam") + val dataMore: List? = + values.continuationContents?.musicPlaylistShelfContinuation?.contents + if (dataMore != null) { + listContent.addAll(dataMore) } + continueParam = + values.continuationContents?.musicPlaylistShelfContinuation?.continuations?.get( + 0, + )?.nextContinuationData?.continuation + count++ + }.onFailure { + Log.e("Continue", "Error: ${it.message}") + continueParam = null + count++ } - Log.d("Repository", "playlist final data: ${listContent.size}") - parseSetVideoId(listContent).let { playlist -> - Log.d("Repository", "playlist final data setVideoId: $playlist") - playlist.forEach { item -> - insertSetVideoId(item) - } - emit(playlist) + } + Log.d("Repository", "playlist final data: ${listContent.size}") + parseSetVideoId(listContent).let { playlist -> + Log.d("Repository", "playlist final data setVideoId: $playlist") + playlist.forEach { item -> + insertSetVideoId(item) } - }.onFailure { e -> - e.printStackTrace() - emit(null) + emit(playlist) } + }.onFailure { e -> + e.printStackTrace() + emit(null) } - }.flowOn(Dispatchers.IO) + } + }.flowOn(Dispatchers.IO) - suspend fun createYouTubePlaylist(playlist: LocalPlaylistEntity): Flow = - flow { - runCatching { - YouTube.createPlaylist(playlist.title, playlist.tracks).onSuccess { - emit(it.playlistId) - }.onFailure { - it.printStackTrace() - emit(null) - } + suspend fun createYouTubePlaylist(playlist: LocalPlaylistEntity): Flow = + flow { + runCatching { + YouTube.createPlaylist(playlist.title, playlist.tracks).onSuccess { + emit(it.playlistId) + }.onFailure { + it.printStackTrace() + emit(null) } } + } - suspend fun editYouTubePlaylist( - title: String, - youtubePlaylistId: String, - ): Flow = - flow { - runCatching { - YouTube.editPlaylist(youtubePlaylistId, title).onSuccess { response -> - emit(response) - }.onFailure { - it.printStackTrace() - emit(0) - } + suspend fun editYouTubePlaylist( + title: String, + youtubePlaylistId: String, + ): Flow = + flow { + runCatching { + YouTube.editPlaylist(youtubePlaylistId, title).onSuccess { response -> + emit(response) + }.onFailure { + it.printStackTrace() + emit(0) } } + } - suspend fun removeYouTubePlaylistItem( - youtubePlaylistId: String, - videoId: String, - ) = flow { - runCatching { - getSetVideoId(videoId).collect { setVideoId -> - if (setVideoId?.setVideoId != null) { - YouTube.removeItemYouTubePlaylist(youtubePlaylistId, videoId, setVideoId.setVideoId).onSuccess { - emit(it) - }.onFailure { - emit(0) - } - } else { + suspend fun removeYouTubePlaylistItem( + youtubePlaylistId: String, + videoId: String, + ) = flow { + runCatching { + getSetVideoId(videoId).collect { setVideoId -> + if (setVideoId?.setVideoId != null) { + YouTube.removeItemYouTubePlaylist( + youtubePlaylistId, + videoId, + setVideoId.setVideoId + ).onSuccess { + emit(it) + }.onFailure { emit(0) } + } else { + emit(0) } } } + } - suspend fun addYouTubePlaylistItem( - youtubePlaylistId: String, - videoId: String, - ) = flow { - runCatching { - YouTube.addPlaylistItem(youtubePlaylistId, videoId).onSuccess { - if (it.playlistEditResults.isNotEmpty()) { - for (playlistEditResult in it.playlistEditResults) { - insertSetVideoId( - SetVideoIdEntity( - playlistEditResult.playlistEditVideoAddedResultData.videoId, - playlistEditResult.playlistEditVideoAddedResultData.setVideoId, - ), - ) - } - emit(it.status) - } else { - emit("FAILED") + suspend fun addYouTubePlaylistItem( + youtubePlaylistId: String, + videoId: String, + ) = flow { + runCatching { + YouTube.addPlaylistItem(youtubePlaylistId, videoId).onSuccess { + if (it.playlistEditResults.isNotEmpty()) { + for (playlistEditResult in it.playlistEditResults) { + insertSetVideoId( + SetVideoIdEntity( + playlistEditResult.playlistEditVideoAddedResultData.videoId, + playlistEditResult.playlistEditVideoAddedResultData.setVideoId, + ), + ) } - }.onFailure { + emit(it.status) + } else { emit("FAILED") } + }.onFailure { + emit("FAILED") } } + } - suspend fun loginToMusixMatch( - email: String, - password: String, - ): Flow = - flow { - runCatching { - if (YouTube.musixmatchUserToken != null && YouTube.musixmatchUserToken != "") { - YouTube.postMusixmatchCredentials(email, password, YouTube.musixmatchUserToken!!).onSuccess { response -> + suspend fun loginToMusixMatch( + email: String, + password: String, + ): Flow = + flow { + runCatching { + if (YouTube.musixmatchUserToken != null && YouTube.musixmatchUserToken != "") { + YouTube.postMusixmatchCredentials( + email, + password, + YouTube.musixmatchUserToken!! + ).onSuccess { response -> + emit(response) + }.onFailure { + it.printStackTrace() + emit(null) + } + } else { + YouTube.getMusixmatchUserToken().onSuccess { usertoken -> + YouTube.musixmatchUserToken = usertoken.message.body.user_token + delay(2000) + YouTube.postMusixmatchCredentials( + email, + password, + YouTube.musixmatchUserToken!! + ).onSuccess { response -> emit(response) }.onFailure { it.printStackTrace() emit(null) } - } else { - YouTube.getMusixmatchUserToken().onSuccess { usertoken -> - YouTube.musixmatchUserToken = usertoken.message.body.user_token - delay(2000) - YouTube.postMusixmatchCredentials(email, password, YouTube.musixmatchUserToken!!).onSuccess { response -> - emit(response) - }.onFailure { - it.printStackTrace() - emit(null) - } - } - .onFailure { throwable -> - throwable.printStackTrace() - emit(null) - } } + .onFailure { throwable -> + throwable.printStackTrace() + emit(null) + } + } + } + } + + suspend fun updateWatchTime( + playbackTrackingVideostatsWatchtimeUrl: String, + watchTimeList: ArrayList, + cpn: String, + playlistId: String?, + ): Flow = + flow { + runCatching { + YouTube.updateWatchTime( + playbackTrackingVideostatsWatchtimeUrl, + watchTimeList, + cpn, + playlistId + ).onSuccess { response -> + emit(response) + }.onFailure { + it.printStackTrace() + emit(0) + } + } + }.flowOn(Dispatchers.IO) + + suspend fun updateWatchTimeFull( + watchTime: String, + cpn: String, + playlistId: String?, + ): Flow = + flow { + runCatching { + YouTube.updateWatchTimeFull(watchTime, cpn, playlistId).onSuccess { response -> + emit(response) + }.onFailure { + it.printStackTrace() + emit(0) } } + }.flowOn(Dispatchers.IO) - suspend fun updateWatchTime( - playbackTrackingVideostatsWatchtimeUrl: String, - watchTimeList: ArrayList, - cpn: String, - playlistId: String?, - ): Flow = - flow { + suspend fun addToYouTubeLiked(mediaId: String?): Flow = + flow { + if (mediaId != null) { runCatching { - YouTube.updateWatchTime(playbackTrackingVideostatsWatchtimeUrl, watchTimeList, cpn, playlistId).onSuccess { response -> - emit(response) + YouTube.addToLiked(mediaId).onSuccess { + Log.d("Liked", "Success: $it") + emit(it) }.onFailure { it.printStackTrace() emit(0) } } - }.flowOn(Dispatchers.IO) - - suspend fun updateWatchTimeFull( - watchTime: String, - cpn: String, - playlistId: String?, - ): Flow = - flow { + } + }.flowOn(Dispatchers.IO) + + suspend fun removeFromYouTubeLiked(mediaId: String?): Flow = + flow { + if (mediaId != null) { runCatching { - YouTube.updateWatchTimeFull(watchTime, cpn, playlistId).onSuccess { response -> - emit(response) + YouTube.removeFromLiked(mediaId).onSuccess { + Log.d("Liked", "Success: $it") + emit(it) }.onFailure { it.printStackTrace() emit(0) } } - }.flowOn(Dispatchers.IO) - - suspend fun addToYouTubeLiked(mediaId: String?): Flow = - flow { - if (mediaId != null) { - runCatching { - YouTube.addToLiked(mediaId).onSuccess { - Log.d("Liked", "Success: $it") - emit(it) - }.onFailure { - it.printStackTrace() - emit(0) - } - } - } - }.flowOn(Dispatchers.IO) - - suspend fun removeFromYouTubeLiked(mediaId: String?): Flow = - flow { - if (mediaId != null) { - runCatching { - YouTube.removeFromLiked(mediaId).onSuccess { - Log.d("Liked", "Success: $it") - emit(it) - }.onFailure { - it.printStackTrace() - emit(0) - } - } - } - }.flowOn(Dispatchers.IO) - } + } + }.flowOn(Dispatchers.IO) +} diff --git a/app/src/main/java/com/maxrave/simpmusic/di/LocalServiceModule.kt b/app/src/main/java/com/maxrave/simpmusic/di/LocalServiceModule.kt index e68393da..9070c355 100644 --- a/app/src/main/java/com/maxrave/simpmusic/di/LocalServiceModule.kt +++ b/app/src/main/java/com/maxrave/simpmusic/di/LocalServiceModule.kt @@ -34,62 +34,72 @@ object LocalServiceModule { @Provides @Singleton - fun provideMusicDatabase(@ApplicationContext context: Context): MusicDatabase = Room.databaseBuilder(context, MusicDatabase::class.java, DB_NAME) - .addTypeConverter(Converters()) - .addMigrations(object : Migration(5, 6) { - override fun migrate(db: SupportSQLiteDatabase) { - val playlistSongMaps = mutableListOf() - db.query("SELECT * FROM local_playlist".toSQLiteQuery()).use { cursor -> - while (cursor.moveToNext()) { - val input = cursor.getString(8) - if (input != null) { - val listType: Type = object : TypeToken?>() {}.type - val tracks = Gson().fromJson?>(input, listType) - Log.w("MIGRATION_5_6", "tracks: $tracks") - tracks?.mapIndexed {index, track -> - if (track != null) { - playlistSongMaps.add( - PairSongLocalPlaylist( - playlistId = cursor.getLong(0), - songId = track, - position = index - ) - ) + fun provideMusicDatabase(@ApplicationContext context: Context): MusicDatabase = + Room.databaseBuilder(context, MusicDatabase::class.java, DB_NAME) + .addTypeConverter(Converters()) + .addMigrations(object : Migration(5, 6) { + override fun migrate(db: SupportSQLiteDatabase) { + val playlistSongMaps = mutableListOf() + db.query("SELECT * FROM local_playlist".toSQLiteQuery()).use { cursor -> + while (cursor.moveToNext()) { + val input = cursor.getString(8) + if (input != null) { + val listType: Type = + object : TypeToken?>() {}.type + val tracks = Gson().fromJson?>(input, listType) + Log.w("MIGRATION_5_6", "tracks: $tracks") + tracks?.mapIndexed { index, track -> + if (track != null) { + playlistSongMaps.add( + PairSongLocalPlaylist( + playlistId = cursor.getLong(0), + songId = track, + position = index + ) + ) + } + } } } } + db.execSQL("ALTER TABLE `format` ADD COLUMN `lengthSeconds` INTEGER DEFAULT NULL") + db.execSQL("ALTER TABLE `format` ADD COLUMN `youtubeCaptionsUrl` TEXT DEFAULT NULL") + db.execSQL("ALTER TABLE `format` ADD COLUMN `cpn` TEXT DEFAULT NULL") + db.execSQL("CREATE TABLE IF NOT EXISTS `pair_song_local_playlist` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` INTEGER NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, `inPlaylist` INTEGER NOT NULL, FOREIGN KEY(`playlistId`) REFERENCES `local_playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`videoId`) ON UPDATE NO ACTION ON DELETE CASCADE )") + db.execSQL("CREATE INDEX IF NOT EXISTS `index_pair_song_local_playlist_playlistId` ON `pair_song_local_playlist` (`playlistId`)") + db.execSQL("CREATE INDEX IF NOT EXISTS `index_pair_song_local_playlist_songId` ON `pair_song_local_playlist` (`songId`)") + playlistSongMaps.forEach { pair -> + db.insert( + table = "pair_song_local_playlist", + conflictAlgorithm = OnConflictStrategy.IGNORE, + values = contentValuesOf( + "playlistId" to pair.playlistId, + "songId" to pair.songId, + "position" to pair.position, + "inPlaylist" to pair.inPlaylist.atZone(ZoneOffset.UTC).toInstant() + .toEpochMilli() + ) + ) + } } - } - db.execSQL("ALTER TABLE `format` ADD COLUMN `lengthSeconds` INTEGER DEFAULT NULL") - db.execSQL("ALTER TABLE `format` ADD COLUMN `youtubeCaptionsUrl` TEXT DEFAULT NULL") - db.execSQL("ALTER TABLE `format` ADD COLUMN `cpn` TEXT DEFAULT NULL") - db.execSQL("CREATE TABLE IF NOT EXISTS `pair_song_local_playlist` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` INTEGER NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, `inPlaylist` INTEGER NOT NULL, FOREIGN KEY(`playlistId`) REFERENCES `local_playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`videoId`) ON UPDATE NO ACTION ON DELETE CASCADE )") - db.execSQL("CREATE INDEX IF NOT EXISTS `index_pair_song_local_playlist_playlistId` ON `pair_song_local_playlist` (`playlistId`)") - db.execSQL("CREATE INDEX IF NOT EXISTS `index_pair_song_local_playlist_songId` ON `pair_song_local_playlist` (`songId`)") - playlistSongMaps.forEach { pair -> - db.insert(table = "pair_song_local_playlist", conflictAlgorithm = OnConflictStrategy.IGNORE, values = contentValuesOf( - "playlistId" to pair.playlistId, - "songId" to pair.songId, - "position" to pair.position, - "inPlaylist" to pair.inPlaylist.atZone(ZoneOffset.UTC).toInstant().toEpochMilli() - )) - } - } - }) - .build() + }) + .build() @Provides @Singleton - fun provideDatabaseDao(musicDatabase: MusicDatabase): DatabaseDao = musicDatabase.getDatabaseDao() + fun provideDatabaseDao(musicDatabase: MusicDatabase): DatabaseDao = + musicDatabase.getDatabaseDao() @Provides @Singleton - fun provideDatastore(@ApplicationContext context: Context): DataStore = context.dataStore + fun provideDatastore(@ApplicationContext context: Context): DataStore = + context.dataStore @Provides @Singleton - fun provideDatastoreManager(settingsDataStore: DataStore): DataStoreManager = DataStoreManager( - settingsDataStore - ) + fun provideDatastoreManager(settingsDataStore: DataStore): DataStoreManager = + DataStoreManager( + settingsDataStore + ) } \ No newline at end of file diff --git a/app/src/main/java/com/maxrave/simpmusic/extension/AllExt.kt b/app/src/main/java/com/maxrave/simpmusic/extension/AllExt.kt index 3b61ca13..eb852629 100644 --- a/app/src/main/java/com/maxrave/simpmusic/extension/AllExt.kt +++ b/app/src/main/java/com/maxrave/simpmusic/extension/AllExt.kt @@ -69,6 +69,7 @@ fun Context.isMyServiceRunning(serviceClass: Class) = try { fun SearchHistory.toQuery(): String { return this.query } + fun List.toQueryList(): ArrayList { val list = ArrayList() for (item in this) { @@ -76,6 +77,7 @@ fun List.toQueryList(): ArrayList { } return list } + fun ResultSong.toTrack(): Track { return Track( album = album, @@ -95,6 +97,7 @@ fun ResultSong.toTrack(): Track { year = "" ) } + fun ResultVideo.toTrack(): Track { return Track( album = null, @@ -120,7 +123,7 @@ fun SongsResult.toTrack(): Track { this.album, this.artists, this.duration ?: "", - this.durationSeconds?: 0, + this.durationSeconds ?: 0, true, this.isExplicit ?: false, "", @@ -134,10 +137,11 @@ fun SongsResult.toTrack(): Track { "" ) } + fun SongItem.toTrack(): Track { return Track( - album = this.album.let { Album(it?.id ?: "", it?.name ?: "")}, - artists = this.artists.map { artist -> Artist(id = artist.id ?: "", name = artist.name) }, + album = this.album.let { Album(it?.id ?: "", it?.name ?: "") }, + artists = this.artists.map { artist -> Artist(id = artist.id ?: "", name = artist.name) }, duration = this.duration.toString(), durationSeconds = this.duration, isAvailable = false, @@ -153,10 +157,11 @@ fun SongItem.toTrack(): Track { year = null ) } + fun VideoItem.toTrack(): Track { return Track( - album = this.album.let { Album(it?.id ?: "", it?.name ?: "")} , - artists = this.artists.map { artist -> Artist(id = artist.id ?: "", name = artist.name) }, + album = this.album.let { Album(it?.id ?: "", it?.name ?: "") }, + artists = this.artists.map { artist -> Artist(id = artist.id ?: "", name = artist.name) }, duration = this.duration.toString(), durationSeconds = this.duration, isAvailable = false, @@ -183,24 +188,27 @@ fun List?.toListTrack(): ArrayList { } return listTrack } + fun List?.toListName(): List { val list = mutableListOf() - if (this != null){ + if (this != null) { for (item in this) { list.add(item.name) } } return list } + fun List?.toListId(): List { val list = mutableListOf() - if (this != null){ + if (this != null) { for (item in this) { list.add(item.id ?: "") } } return list } + fun List.connectArtists(): String { val stringBuilder = StringBuilder() @@ -214,6 +222,7 @@ fun List.connectArtists(): String { return stringBuilder.toString() } + fun Track.toSongEntity(): SongEntity { return SongEntity( videoId = this.videoId, @@ -255,7 +264,7 @@ fun String?.removeDuplicateWords(): String { fun SongEntity.toTrack(): Track { val listArtist = mutableListOf() - if (this.artistName != null ) { + if (this.artistName != null) { for (i in 0 until this.artistName.size) { listArtist.add(Artist(this.artistId?.get(i) ?: "", this.artistName[i])) } @@ -268,7 +277,7 @@ fun SongEntity.toTrack(): Track { isAvailable = this.isAvailable, isExplicit = this.isExplicit, likeStatus = this.likeStatus, - thumbnails = listOf(Thumbnail(720, this.thumbnails ?: "",1080)), + thumbnails = listOf(Thumbnail(720, this.thumbnails ?: "", 1080)), title = this.title, videoId = this.videoId, videoType = this.videoType, @@ -278,6 +287,7 @@ fun SongEntity.toTrack(): Track { year = "" ) } + fun List?.toArrayListTrack(): ArrayList { val listTrack: ArrayList = arrayListOf() if (this != null) { @@ -295,7 +305,7 @@ fun MediaItem?.toSongEntity(): SongEntity? { albumName = this.mediaMetadata.albumTitle.toString(), artistId = null, artistName = listOf(this.mediaMetadata.artist.toString()), - duration = "", + duration = "", durationSeconds = 0, isAvailable = true, isExplicit = false, @@ -372,19 +382,20 @@ fun VideosResult.toTrack(): Track { return Track( album = null, artists = this.artists, - duration = this.duration?: "", - durationSeconds = this.durationSeconds?: 0, + duration = this.duration ?: "", + durationSeconds = this.durationSeconds ?: 0, isAvailable = true, isExplicit = false, likeStatus = "INDIFFERENT", thumbnails = thumbList, title = this.title, videoId = this.videoId, - videoType = this.videoType?: "", + videoType = this.videoType ?: "", category = this.category, feedbackTokens = null, resultType = this.resultType, - year = "") + year = "" + ) } @JvmName("VideoResulttoTrack") @@ -415,6 +426,7 @@ fun Content.toTrack(): Track { year = "" ) } + fun List.toListVideoId(): List { val list = mutableListOf() for (item in this) { @@ -423,7 +435,7 @@ fun List.toListVideoId(): List { return list } -fun AlbumBrowse.toAlbumEntity(id : String): AlbumEntity { +fun AlbumBrowse.toAlbumEntity(id: String): AlbumEntity { return AlbumEntity( browseId = id, artistId = this.artists.toListId(), @@ -466,7 +478,13 @@ fun Track.addThumbnails(): Track { isAvailable = this.isAvailable, isExplicit = this.isExplicit, likeStatus = this.likeStatus, - thumbnails = listOf(Thumbnail(720, "https://i.ytimg.com/vi/${this.videoId}/maxresdefault.jpg", 1280)), + thumbnails = listOf( + Thumbnail( + 720, + "https://i.ytimg.com/vi/${this.videoId}/maxresdefault.jpg", + 1280 + ) + ), title = this.title, videoId = this.videoId, videoType = this.videoType, @@ -601,7 +619,13 @@ fun PipedResponse.toTrack(videoId: String): Track { isAvailable = false, isExplicit = false, likeStatus = "INDIFFERENT", - thumbnails = listOf(Thumbnail(720, this.thumbnailUrl ?: "https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg", 1080)), + thumbnails = listOf( + Thumbnail( + 720, + this.thumbnailUrl ?: "https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg", + 1080 + ) + ), title = this.title ?: " ", videoId = videoId, videoType = "Song", @@ -638,16 +662,21 @@ fun YouTubeInitialPage.toTrack(): Track { year = "" ) } + fun MusixmatchTranslationLyricsResponse.toLyrics(originalLyrics: Lyrics): Lyrics? { if (this.message.body.translations_list.isEmpty()) { return null - } - else { + } else { val listTranslation = this.message.body.translations_list val translation = originalLyrics.copy( lines = originalLyrics.lines?.mapIndexed { index, line -> line.copy( - words = if (!line.words.contains("♫")) {listTranslation.find { it.translation.matched_line == line.words || it.translation.subtitle_matched_line == line.words || it.translation.snippet == line.words }?.translation?.description ?: ""} else {line.words} + words = if (!line.words.contains("♫")) { + listTranslation.find { it.translation.matched_line == line.words || it.translation.subtitle_matched_line == line.words || it.translation.snippet == line.words }?.translation?.description + ?: "" + } else { + line.words + } ) } ) @@ -681,6 +710,7 @@ fun NavController.navigateSafe(resId: Int, bundle: Bundle? = null) { } } } + fun zip(first: LiveData, second: LiveData): Flow> { val mediatorLiveData = MediatorLiveData>() @@ -786,6 +816,12 @@ fun View.fadInAnimation(duration: Long = 300, completion: (() -> Unit)? = null) } } +infix fun Collection.symmetricDifference(other: Collection): Set { + val left = this subtract other + val right = other subtract this + return left union right +} + operator fun File.div(child: String): File = File(this, child) fun String.toSQLiteQuery(): SimpleSQLiteQuery = SimpleSQLiteQuery(this) fun InputStream.zipInputStream(): ZipInputStream = ZipInputStream(this) diff --git a/app/src/main/java/com/maxrave/simpmusic/service/SimpleMediaService.kt b/app/src/main/java/com/maxrave/simpmusic/service/SimpleMediaService.kt index 4ae63c4e..04de32a3 100644 --- a/app/src/main/java/com/maxrave/simpmusic/service/SimpleMediaService.kt +++ b/app/src/main/java/com/maxrave/simpmusic/service/SimpleMediaService.kt @@ -84,7 +84,12 @@ class SimpleMediaService : MediaLibraryService() { super.onCreate() Log.w("Service", "Simple Media Service Created") setMediaNotificationProvider( - DefaultMediaNotificationProvider(this, { MEDIA_NOTIFICATION.NOTIFICATION_ID }, MEDIA_NOTIFICATION.NOTIFICATION_CHANNEL_ID, R.string.notification_channel_name) + DefaultMediaNotificationProvider( + this, + { MEDIA_NOTIFICATION.NOTIFICATION_ID }, + MEDIA_NOTIFICATION.NOTIFICATION_CHANNEL_ID, + R.string.notification_channel_name + ) .apply { setSmallIcon(R.drawable.mono) } @@ -268,7 +273,13 @@ class SimpleMediaService : MediaLibraryService() { @UnstableApi fun provideMediaSourceFactory(): DefaultMediaSourceFactory = DefaultMediaSourceFactory( - provideResolvingDataSourceFactory(provideCacheDataSource(downloadCache, playerCache), downloadCache, playerCache, mainRepository, dataStoreManager), + provideResolvingDataSourceFactory( + provideCacheDataSource(downloadCache, playerCache), + downloadCache, + playerCache, + mainRepository, + dataStoreManager + ), provideExtractorFactory() ) @@ -325,7 +336,8 @@ class SimpleMediaService : MediaLibraryService() { MediaSession.Builder(context, player) .setCallback(callback) .setSessionActivity( - PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java), + PendingIntent.getActivity( + context, 0, Intent(context, MainActivity::class.java), PendingIntent.FLAG_IMMUTABLE ) ) diff --git a/app/src/main/java/com/maxrave/simpmusic/service/test/notification/NotificationHandler.kt b/app/src/main/java/com/maxrave/simpmusic/service/test/notification/NotificationHandler.kt new file mode 100644 index 00000000..557e758e --- /dev/null +++ b/app/src/main/java/com/maxrave/simpmusic/service/test/notification/NotificationHandler.kt @@ -0,0 +1,101 @@ +package com.maxrave.simpmusic.service.test.notification + +import android.Manifest +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.drawable.BitmapDrawable +import androidx.annotation.OptIn +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.app.ActivityCompat +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.graphics.drawable.toBitmap +import androidx.media3.common.util.UnstableApi +import coil.ImageLoader +import coil.request.ImageRequest +import coil.request.SuccessResult +import com.maxrave.simpmusic.R +import com.maxrave.simpmusic.ui.MainActivity +import kotlinx.coroutines.runBlocking + +object NotificationHandler { + private const val CHANNEL_ID = "transactions_reminder_channel" + + @OptIn(UnstableApi::class) + fun createReminderNotification(context: Context, noti: NotificationModel) { + // No back-stack when launched + val intent = Intent(context, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + val pendingIntent = PendingIntent.getActivity( + context, 0, intent, + PendingIntent.FLAG_IMMUTABLE + ) + + val bitmap = runBlocking { + val loader = ImageLoader(context) + val request = ImageRequest.Builder(context) + .data( + noti.single.firstOrNull()?.thumbnails?.lastOrNull()?.url + ?: noti.album.firstOrNull()?.thumbnails?.lastOrNull()?.url + ) + .allowHardware(false) // Disable hardware bitmaps. + .build() + + return@runBlocking when (val result = loader.execute(request)) { + is SuccessResult -> ((result as SuccessResult).drawable as BitmapDrawable).bitmap + else -> AppCompatResources.getDrawable(context, R.drawable.holder) + ?.toBitmap(128, 128) + } + } + val builder = NotificationCompat.Builder(context, CHANNEL_ID) + .setSmallIcon(R.drawable.mono) + .setContentTitle(noti.name) + .setContentText( + if (noti.single.isNotEmpty()) { + "${context.getString(R.string.new_singles)}: ${noti.single.joinToString { it.title }}" + } else { + "${context.getString(R.string.new_albums)}: ${noti.album.joinToString { it.title }}" + } + ) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setLargeIcon(bitmap) + .setContentIntent(pendingIntent) // For launching the MainActivity + .setAutoCancel(true) // Remove notification when tapped + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) // Show on lock screen + with(NotificationManagerCompat.from(context)) { + if (ActivityCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS + ) != PackageManager.PERMISSION_GRANTED + ) { + + return + } + notify(noti.hashCode(), builder.build()) + } + } + + /** + * Required on Android O+ + */ + fun createNotificationChannel(context: Context) { + val notificationManager: NotificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + if (notificationManager.getNotificationChannel(CHANNEL_ID) == null) { + val name = "Update Followed Artists" + val descriptionText = "This channel sends daily reminders to add your transactions" + val importance = NotificationManager.IMPORTANCE_HIGH + val channel = NotificationChannel(CHANNEL_ID, name, importance).apply { + description = descriptionText + } + // Register the channel with the system + + notificationManager.createNotificationChannel(channel) + } + } +} diff --git a/app/src/main/java/com/maxrave/simpmusic/service/test/notification/NotifyWork.kt b/app/src/main/java/com/maxrave/simpmusic/service/test/notification/NotifyWork.kt new file mode 100644 index 00000000..0e1d2a5a --- /dev/null +++ b/app/src/main/java/com/maxrave/simpmusic/service/test/notification/NotifyWork.kt @@ -0,0 +1,141 @@ +package com.maxrave.simpmusic.service.test.notification + +import android.content.Context +import android.util.Log +import androidx.hilt.work.HiltWorker +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.maxrave.simpmusic.data.db.entities.ArtistEntity +import com.maxrave.simpmusic.data.db.entities.FollowedArtistSingleAndAlbum +import com.maxrave.simpmusic.data.model.browse.artist.ResultAlbum +import com.maxrave.simpmusic.data.model.browse.artist.ResultSingle +import com.maxrave.simpmusic.data.repository.MainRepository +import com.maxrave.simpmusic.extension.symmetricDifference +import com.maxrave.simpmusic.utils.Resource +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withContext +import javax.inject.Inject + +@HiltWorker +class NotifyWork @AssistedInject constructor( + @Assisted context: Context, + @Assisted params: WorkerParameters +) : CoroutineWorker(context, params) { + + @Inject + lateinit var mainRepository: MainRepository + + private val mapOfNotification = arrayListOf() + + override suspend fun doWork(): Result { + + return withContext(Dispatchers.IO) { + Log.w("NotifyWork", "doWork: ") + val artistList: List = mainRepository.getFollowedArtists().first() + val listFollowedArtistSingleAndAlbum = + mainRepository.getAllFollowedArtistSingleAndAlbums().first() ?: listOf() + Log.w("NotifyWork", "doWork: $artistList") + Log.w("NotifyWork", "doWork: $listFollowedArtistSingleAndAlbum") + artistList.forEach { art -> + mainRepository.getArtistData(art.channelId).first().let { artistData -> + if (artistData is Resource.Success) { + val artist = artistData.data + if (artist != null) { + val single = artist.singles?.results + val album = artist.albums?.results + val savedSingle = + listFollowedArtistSingleAndAlbum.find { it.channelId == art.channelId }?.single + val savedAlbum = + listFollowedArtistSingleAndAlbum.find { it.channelId == art.channelId }?.album + if (!savedSingle.isNullOrEmpty() && single != null) { + val differentSingle = + single.map { it.browseId } symmetricDifference (savedSingle.map { it["browseId"] }) + mapOfNotification.add( + NotificationModel( + name = art.name, + channelId = art.channelId, + single = single.filter { + differentSingle.contains(it.browseId) + }, + album = listOf() + ) + ) + + } + if (!savedAlbum.isNullOrEmpty() && album != null) { + val differentAlbum = + album.map { it.browseId } symmetricDifference (savedAlbum.map { it["browseId"] }) + mapOfNotification.add( + NotificationModel( + name = art.name, + channelId = art.channelId, + single = listOf(), + album = album.filter { + differentAlbum.contains(it.browseId) + } + ) + ) + } + mainRepository.insertFollowedArtistSingleAndAlbum( + FollowedArtistSingleAndAlbum( + channelId = art.channelId, + name = art.name, + single = single.toMap(), + album = album.toMap() + ) + ) + } + } + } + } + Log.w("NotifyWork", "doWork: $mapOfNotification") + NotificationHandler.createNotificationChannel(applicationContext) + mapOfNotification.forEach { noti -> + if (noti.album.isNotEmpty() || noti.single.isNotEmpty()) { + NotificationHandler.createReminderNotification( + applicationContext, noti + ) + } + } + Result.success() + } + } +} + +private fun List?.toMap(): List> { + return when (this?.firstOrNull()) { + is ResultSingle -> { + this.map { + val single = it as ResultSingle + mapOf( + "browseId" to single.browseId, + "title" to single.title, + "thumbnails" to (single.thumbnails.lastOrNull()?.url ?: "") + ) + } + } + + is ResultAlbum -> { + this.map { + val album = it as ResultAlbum + mapOf( + "browseId" to album.browseId, + "title" to album.title, + "thumbnails" to (album.thumbnails.lastOrNull()?.url ?: "") + ) + } + } + + else -> listOf() + } +} + +data class NotificationModel( + val name: String, + val channelId: String, + val single: List, + val album: List +) \ No newline at end of file diff --git a/app/src/main/java/com/maxrave/simpmusic/ui/MainActivity.kt b/app/src/main/java/com/maxrave/simpmusic/ui/MainActivity.kt index d771afe6..97f3370f 100644 --- a/app/src/main/java/com/maxrave/simpmusic/ui/MainActivity.kt +++ b/app/src/main/java/com/maxrave/simpmusic/ui/MainActivity.kt @@ -388,6 +388,7 @@ class MainActivity : AppCompatActivity() { // WindowCompat.setDecorFitsSystemWindows(window, false) enableEdgeToEdge() viewModel.checkIsRestoring() + viewModel.runWorker() // } else { // window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN // } diff --git a/app/src/main/java/com/maxrave/simpmusic/viewModel/SharedViewModel.kt b/app/src/main/java/com/maxrave/simpmusic/viewModel/SharedViewModel.kt index 1bf83ef1..bcb103ff 100644 --- a/app/src/main/java/com/maxrave/simpmusic/viewModel/SharedViewModel.kt +++ b/app/src/main/java/com/maxrave/simpmusic/viewModel/SharedViewModel.kt @@ -18,6 +18,11 @@ import androidx.media3.common.MediaMetadata import androidx.media3.common.util.UnstableApi import androidx.media3.datasource.cache.SimpleCache import androidx.media3.exoplayer.offline.Download +import androidx.work.Constraints +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.NetworkType +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager import coil.ImageLoader import coil.request.ImageRequest import com.maxrave.kotlinytmusicscraper.YouTube @@ -66,6 +71,7 @@ import com.maxrave.simpmusic.service.RepeatState import com.maxrave.simpmusic.service.SimpleMediaServiceHandler import com.maxrave.simpmusic.service.SimpleMediaState import com.maxrave.simpmusic.service.test.download.DownloadUtils +import com.maxrave.simpmusic.service.test.notification.NotifyWork import com.maxrave.simpmusic.ui.widget.BasicWidget import com.maxrave.simpmusic.utils.Resource import dagger.hilt.android.lifecycle.HiltViewModel @@ -88,7 +94,12 @@ import javax.inject.Inject @HiltViewModel @UnstableApi -class SharedViewModel @Inject constructor(private var dataStoreManager: DataStoreManager, @DownloadCache private val downloadedCache: SimpleCache, private val mainRepository: MainRepository, private val application: Application) : AndroidViewModel(application) { +class SharedViewModel @Inject constructor( + private var dataStoreManager: DataStoreManager, + @DownloadCache private val downloadedCache: SimpleCache, + private val mainRepository: MainRepository, + private val application: Application +) : AndroidViewModel(application) { var isFirstLiked: Boolean = false var isFirstMiniplayer: Boolean = false var isFirstSuggestions: Boolean = false @@ -130,7 +141,7 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor private var _progressMillis = MutableStateFlow(0L) val progressMillis: SharedFlow = _progressMillis.asSharedFlow() val progress: SharedFlow = _progress.asSharedFlow() - private var _progressString : MutableStateFlow = MutableStateFlow("00:00") + private var _progressString: MutableStateFlow = MutableStateFlow("00:00") val progressString: SharedFlow = _progressString.asSharedFlow() private val _duration = MutableStateFlow(0L) @@ -142,7 +153,8 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor var notReady = MutableLiveData(true) var _lyrics = MutableStateFlow?>(null) -// val lyrics: LiveData> = _lyrics + + // val lyrics: LiveData> = _lyrics private var lyricsFormat: MutableLiveData> = MutableLiveData() var lyricsFull = MutableLiveData() @@ -271,6 +283,7 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor _bufferedPercentage.value = mediaState.bufferedPercentage _duration.value = mediaState.duration } + is SimpleMediaState.Ready -> { notReady.value = false _duration.value = mediaState.duration @@ -411,11 +424,16 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor } } val job7 = launch { - format.collect {formatTemp -> + format.collect { formatTemp -> if (dataStoreManager.sendBackToGoogle.first() == TRUE) { if (formatTemp != null) { println("format in viewModel: $formatTemp") - initPlayback(formatTemp.playbackTrackingVideostatsPlaybackUrl, formatTemp.playbackTrackingAtrUrl, formatTemp.playbackTrackingVideostatsWatchtimeUrl, formatTemp.cpn) + initPlayback( + formatTemp.playbackTrackingVideostatsPlaybackUrl, + formatTemp.playbackTrackingAtrUrl, + formatTemp.playbackTrackingVideostatsWatchtimeUrl, + formatTemp.cpn + ) } } resetLyrics() @@ -499,11 +517,11 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor if (it.first == 204) { Log.d("Check initPlayback", "Success") watchTimeList.add(0f) - watchTimeList.add(5.54f) - watchTimeList.add(it.second) - updateWatchTime() + watchTimeList.add(5.54f) + watchTimeList.add(it.second) + updateWatchTime() + } } - } } } } @@ -517,20 +535,28 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor if (second in watchTimeList.last()..watchTimeList.last() + 1.2f) { val watchTimeUrl = _format.value?.playbackTrackingVideostatsWatchtimeUrl val cpn = _format.value?.cpn - if (second + 20.23f < (duration.first()/1000).toFloat()) { + if (second + 20.23f < (duration.first() / 1000).toFloat()) { watchTimeList.add(second + 20.23f) if (watchTimeUrl != null && cpn != null) { - mainRepository.updateWatchTime(watchTimeUrl, watchTimeList, cpn, playlistId.value).collect { response -> + mainRepository.updateWatchTime( + watchTimeUrl, + watchTimeList, + cpn, + playlistId.value + ).collect { response -> if (response == 204) { Log.d("Check updateWatchTime", "Success") } } } - } - else { + } else { watchTimeList.clear() if (watchTimeUrl != null && cpn != null) { - mainRepository.updateWatchTimeFull(watchTimeUrl, cpn, playlistId.value).collect { response -> + mainRepository.updateWatchTimeFull( + watchTimeUrl, + cpn, + playlistId.value + ).collect { response -> if (response == 204) { Log.d("Check updateWatchTimeFull", "Success") } @@ -558,6 +584,7 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor _sleepTimerRunning.value = true simpleMediaServiceHandler!!.sleepStart(minutes) } + fun stopSleepTimer() { _sleepTimerRunning.value = false simpleMediaServiceHandler!!.sleepStop() @@ -577,29 +604,41 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor if (down != null) { when (down.state) { Download.STATE_COMPLETED -> { - mainRepository.getSongById(videoId).collect{ song -> + mainRepository.getSongById(videoId).collect { song -> if (song?.downloadState != DownloadState.STATE_DOWNLOADED) { - mainRepository.updateDownloadState(videoId, DownloadState.STATE_DOWNLOADED) + mainRepository.updateDownloadState( + videoId, + DownloadState.STATE_DOWNLOADED + ) } } Log.d("Check Downloaded", "Downloaded") } + Download.STATE_FAILED -> { - mainRepository.getSongById(videoId).collect{ song -> + mainRepository.getSongById(videoId).collect { song -> if (song?.downloadState != DownloadState.STATE_NOT_DOWNLOADED) { - mainRepository.updateDownloadState(videoId, DownloadState.STATE_NOT_DOWNLOADED) + mainRepository.updateDownloadState( + videoId, + DownloadState.STATE_NOT_DOWNLOADED + ) } } Log.d("Check Downloaded", "Failed") } + Download.STATE_DOWNLOADING -> { - mainRepository.getSongById(videoId).collect{ song -> + mainRepository.getSongById(videoId).collect { song -> if (song?.downloadState != DownloadState.STATE_DOWNLOADING) { - mainRepository.updateDownloadState(videoId, DownloadState.STATE_DOWNLOADING) + mainRepository.updateDownloadState( + videoId, + DownloadState.STATE_DOWNLOADING + ) } } Log.d("Check Downloaded", "Downloading ${down.percentDownloaded}") } + else -> { Log.d("Check Downloaded", "${down.state}") } @@ -664,11 +703,13 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor } } } + fun insertLyrics(lyrics: LyricsEntity) { viewModelScope.launch { mainRepository.insertLyrics(lyrics) } } + fun getSkipSegments(videoId: String) { resetSkipSegments() viewModelScope.launch { @@ -676,16 +717,17 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor if (segments != null) { Log.w("Check segments ${videoId}", segments.toString()) _skipSegments.value = segments - } - else { + } else { _skipSegments.value = null } } } } + private fun resetSkipSegments() { _skipSegments.value = null } + fun getSavedLyrics(track: Track, query: String) { viewModelScope.launch { resetLyrics() @@ -696,12 +738,11 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor val lyricsData = lyrics.toLyrics() Log.d("Check Lyrics In DB", lyricsData.toString()) parseLyrics(lyricsData) - } - else { + } else { resetLyrics() mainRepository.getLyricsData(query, track.durationSeconds).collect { response -> _lyrics.value = response.second - when(_lyrics.value) { + when (_lyrics.value) { is Resource.Success -> { if (_lyrics.value?.data != null) { _lyricsProvider.value = LyricsProvider.MUSIXMATCH @@ -718,6 +759,7 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor } } } + else -> { Log.d("Check lyrics", "Loading") } @@ -728,14 +770,15 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor } } - fun getRelated(videoId: String){ + fun getRelated(videoId: String) { Queue.clear() viewModelScope.launch { - mainRepository.getRelatedData(videoId).collect{ response -> + mainRepository.getRelatedData(videoId).collect { response -> _related.value = response } } } + fun getCurrentMediaItem(): MediaItem? { _nowPlayingMediaItem.value = simpleMediaServiceHandler?.getCurrentMediaItem() return simpleMediaServiceHandler?.getCurrentMediaItem() @@ -744,10 +787,12 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor fun getCurrentMediaItemIndex(): Int { return simpleMediaServiceHandler?.currentIndex() ?: 0 } + @UnstableApi - fun playMediaItemInMediaSource(index: Int){ + fun playMediaItemInMediaSource(index: Int) { simpleMediaServiceHandler?.playMediaItemInMediaSource(index) } + @UnstableApi fun loadMediaItemFromTrack(track: Track, type: String, index: Int? = null) { quality = runBlocking { dataStoreManager.quality.first() } @@ -810,46 +855,49 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor // mainRepository.getStream(track.videoId, itag).collect{ stream -> // if (stream != null){ // uri = stream - Log.d("Check URI", uri) - val artistName: String = track.artists.toListName().connectArtists() - var thumbUrl = track.thumbnails?.last()?.url!! - if (thumbUrl.contains("w120")) { - thumbUrl = Regex("([wh])120").replace(thumbUrl, "$1544") - } - Log.d("Check URI", uri) - simpleMediaServiceHandler?.addMediaItem( - MediaItem.Builder() - .setUri(track.videoId) - .setMediaId(track.videoId) - .setCustomCacheKey(track.videoId) - .setMediaMetadata( - MediaMetadata.Builder() - .setTitle(track.title) - .setArtist(artistName) - .setArtworkUri(thumbUrl.toUri()) - .setAlbumTitle(track.album?.name) - .build() - ) - .build(), - type != RECOVER_TRACK_QUEUE - ) - _nowPlayingMediaItem.value = getCurrentMediaItem() - Log.d( - "Check MediaItem Thumbnail", - getCurrentMediaItem()?.mediaMetadata?.artworkUri.toString() + Log.d("Check URI", uri) + val artistName: String = track.artists.toListName().connectArtists() + var thumbUrl = track.thumbnails?.last()?.url!! + if (thumbUrl.contains("w120")) { + thumbUrl = Regex("([wh])120").replace(thumbUrl, "$1544") + } + Log.d("Check URI", uri) + simpleMediaServiceHandler?.addMediaItem( + MediaItem.Builder() + .setUri(track.videoId) + .setMediaId(track.videoId) + .setCustomCacheKey(track.videoId) + .setMediaMetadata( + MediaMetadata.Builder() + .setTitle(track.title) + .setArtist(artistName) + .setArtworkUri(thumbUrl.toUri()) + .setAlbumTitle(track.album?.name) + .build() ) - simpleMediaServiceHandler?.addFirstMetadata(track) + .build(), + type != RECOVER_TRACK_QUEUE + ) + _nowPlayingMediaItem.value = getCurrentMediaItem() + Log.d( + "Check MediaItem Thumbnail", + getCurrentMediaItem()?.mediaMetadata?.artworkUri.toString() + ) + simpleMediaServiceHandler?.addFirstMetadata(track) } when (type) { SONG_CLICK -> { getRelated(track.videoId) } + VIDEO_CLICK -> { getRelated(track.videoId) } + SHARE -> { getRelated(track.videoId) } + PLAYLIST_CLICK -> { if (index == null) { // fetchSourceFromQueue(downloaded = downloaded ?: 0) @@ -859,6 +907,7 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor loadPlaylistOrAlbum(index = index) } } + ALBUM_CLICK -> { Queue.setContinuation(null) if (index == null) { @@ -869,6 +918,7 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor loadPlaylistOrAlbum(index = index) } } + RECOVER_TRACK_QUEUE -> { if (getString(RESTORE_LAST_PLAYED_TRACK_AND_QUEUE_DONE) == DataStoreManager.FALSE) { recentPosition = runBlocking { dataStoreManager.recentPosition.first() } @@ -879,13 +929,12 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor if (songDB.value?.duration != null) { if (songDB.value?.duration != "" && songDB.value?.duration?.contains(":") == true) { songDB.value?.duration?.split(":")?.let { split -> - _duration.emit(((split[0].toInt() * 60) + split[1].toInt())*1000.toLong()) + _duration.emit(((split[0].toInt() * 60) + split[1].toInt()) * 1000.toLong()) Log.d("Check Duration", _duration.value.toString()) calculateProgressValues(recentPosition.toLong()) } } - } - else { + } else { simpleMediaServiceHandler?.getPlayerDuration()?.let { _duration.emit(it) calculateProgressValues(recentPosition.toLong()) @@ -916,18 +965,22 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor ) ) } + UIEvent.Repeat -> simpleMediaServiceHandler?.onPlayerEvent(PlayerEvent.Repeat) UIEvent.Shuffle -> simpleMediaServiceHandler?.onPlayerEvent(PlayerEvent.Shuffle) } } + fun formatDuration(duration: Long): String { val minutes: Long = TimeUnit.MINUTES.convert(duration, TimeUnit.MILLISECONDS) val seconds: Long = (TimeUnit.SECONDS.convert(duration, TimeUnit.MILLISECONDS) - minutes * TimeUnit.SECONDS.convert(1, TimeUnit.MINUTES)) return String.format("%02d:%02d", minutes, seconds) } + private fun calculateProgressValues(currentProgress: Long) { - _progress.value = if (currentProgress > 0) (currentProgress.toFloat() / _duration.value) else 0f + _progress.value = + if (currentProgress > 0) (currentProgress.toFloat() / _duration.value) else 0f _progressString.value = formatDuration(currentProgress) } @@ -940,79 +993,87 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor } } } + fun updateLocalPlaylistTracks(list: List, id: Long) { viewModelScope.launch { mainRepository.getSongsByListVideoId(list).collect { values -> var count = 0 values.forEach { song -> - if (song.downloadState == DownloadState.STATE_DOWNLOADED){ + if (song.downloadState == DownloadState.STATE_DOWNLOADED) { count++ } } mainRepository.updateLocalPlaylistTracks(list, id) - Toast.makeText(getApplication(), application.getString(R.string.added_to_playlist), Toast.LENGTH_SHORT).show() + Toast.makeText( + getApplication(), + application.getString(R.string.added_to_playlist), + Toast.LENGTH_SHORT + ).show() if (count == values.size) { - mainRepository.updateLocalPlaylistDownloadState(DownloadState.STATE_DOWNLOADED, id) - } - else { - mainRepository.updateLocalPlaylistDownloadState(DownloadState.STATE_NOT_DOWNLOADED, id) + mainRepository.updateLocalPlaylistDownloadState( + DownloadState.STATE_DOWNLOADED, + id + ) + } else { + mainRepository.updateLocalPlaylistDownloadState( + DownloadState.STATE_NOT_DOWNLOADED, + id + ) } } } } - fun parseLyrics(lyrics: Lyrics?){ - if (lyrics != null){ - if (!lyrics.error){ - if (lyrics.syncType == "LINE_SYNCED") - { + fun parseLyrics(lyrics: Lyrics?) { + if (lyrics != null) { + if (!lyrics.error) { + if (lyrics.syncType == "LINE_SYNCED") { val firstLine = Line("0", "0", listOf(), "") val lines: ArrayList = ArrayList() lines.addAll(lyrics.lines as ArrayList) lines.add(0, firstLine) lyricsFormat.postValue(lines) var txt = "" - for (line in lines){ - txt += if (line == lines.last()){ + for (line in lines) { + txt += if (line == lines.last()) { line.words - } else{ + } else { line.words + "\n" } } lyricsFull.postValue(txt) // Log.d("Check Lyrics", lyricsFormat.value.toString()) - } - else if (lyrics.syncType == "UNSYNCED"){ + } else if (lyrics.syncType == "UNSYNCED") { val lines: ArrayList = ArrayList() lines.addAll(lyrics.lines as ArrayList) var txt = "" - for (line in lines){ - if (line == lines.last()){ + for (line in lines) { + if (line == lines.last()) { txt += line.words - } - else{ + } else { txt += line.words + "\n" } } lyricsFormat.postValue(arrayListOf(Line("0", "0", listOf(), txt))) lyricsFull.postValue(txt) } - } - else { + } else { val lines = Line("0", "0", listOf(), "Lyrics not found") lyricsFormat.postValue(arrayListOf(lines)) // Log.d("Check Lyrics", "Lyrics not found") } } } + fun getLyricsSyncState(): Config.SyncState { - return when(_lyrics.value?.data?.syncType) { + return when (_lyrics.value?.data?.syncType) { null -> Config.SyncState.NOT_FOUND "LINE_SYNCED" -> Config.SyncState.LINE_SYNCED "UNSYNCED" -> Config.SyncState.UNSYNCED else -> Config.SyncState.NOT_FOUND } } + fun getActiveLyrics(current: Long): Int? { val lyricsFormat = _lyrics.value?.data?.lines lyricsFormat?.indices?.forEach { i -> @@ -1102,13 +1163,17 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor fun changeAllDownloadingToError() { viewModelScope.launch { - mainRepository.getDownloadingSongs().collect {songs -> + mainRepository.getDownloadingSongs().collect { songs -> songs?.forEach { song -> - mainRepository.updateDownloadState(song.videoId, DownloadState.STATE_NOT_DOWNLOADED) + mainRepository.updateDownloadState( + song.videoId, + DownloadState.STATE_NOT_DOWNLOADED + ) } } } } + private val _songFull: MutableLiveData = MutableLiveData() var songFull: LiveData = _songFull @@ -1138,7 +1203,7 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor recentPosition = runBlocking { (dataStoreManager.recentPosition.first()) } } - fun getSaveLastPlayedSong () { + fun getSaveLastPlayedSong() { viewModelScope.launch { dataStoreManager.saveRecentSongAndQueue.first().let { saved -> Log.d("Check SaveLastPlayedSong", restoreLastPlayedTrackDone.toString()) @@ -1146,12 +1211,13 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor } } } + private var _savedQueue: MutableLiveData> = MutableLiveData() val savedQueue: LiveData> = _savedQueue fun getSavedSongAndQueue() { viewModelScope.launch { - dataStoreManager.recentMediaId.first().let{ mediaId -> - mainRepository.getSongById(mediaId).collect {song -> + dataStoreManager.recentMediaId.first().let { mediaId -> + mainRepository.getSongById(mediaId).collect { song -> if (song != null) { Queue.clear() Queue.setNowPlaying(song.toTrack()) @@ -1161,6 +1227,7 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor } } } + private fun getSaveQueue() { viewModelScope.launch { mainRepository.getSavedQueue().collect { queue -> @@ -1175,14 +1242,20 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor fun checkAllDownloadingSongs() { viewModelScope.launch { - mainRepository.getDownloadingSongs().collect {songs -> + mainRepository.getDownloadingSongs().collect { songs -> songs?.forEach { song -> - mainRepository.updateDownloadState(song.videoId, DownloadState.STATE_NOT_DOWNLOADED) + mainRepository.updateDownloadState( + song.videoId, + DownloadState.STATE_NOT_DOWNLOADED + ) } } - mainRepository.getPreparingSongs().collect {songs -> + mainRepository.getPreparingSongs().collect { songs -> songs.forEach { song -> - mainRepository.updateDownloadState(song.videoId, DownloadState.STATE_NOT_DOWNLOADED) + mainRepository.updateDownloadState( + song.videoId, + DownloadState.STATE_NOT_DOWNLOADED + ) } } } @@ -1194,8 +1267,7 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor if (cookie != "") { YouTube.cookie = cookie Log.d("Cookie", "Cookie is not empty") - } - else { + } else { Log.e("Cookie", "Cookie is empty") } } @@ -1203,8 +1275,7 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor if (cookie != "") { YouTube.musixMatchCookie = cookie Log.d("Musixmatch", "Cookie is not empty") - } - else { + } else { Log.e("Musixmatch", "Cookie is empty") } } @@ -1213,7 +1284,7 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor fun getFormat(mediaId: String?) { viewModelScope.launch { - if (mediaId != null){ + if (mediaId != null) { mainRepository.getNewFormat(mediaId).collect { f -> if (f != null) { _format.emit(f) @@ -1263,6 +1334,7 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor fun skipSegment(position: Long) { simpleMediaServiceHandler?.skipSegment(position) } + fun sponsorBlockEnabled() = runBlocking { dataStoreManager.sponsorBlockEnabled.first() } fun sponsorBlockCategories() = runBlocking { dataStoreManager.getSponsorBlockCategories() } fun stopPlayer() { @@ -1271,15 +1343,31 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor fun addToYouTubePlaylist(localPlaylistId: Long, youtubePlaylistId: String, videoId: String) { viewModelScope.launch { - mainRepository.updateLocalPlaylistYouTubePlaylistSyncState(localPlaylistId, LocalPlaylistEntity.YouTubeSyncState.Syncing) + mainRepository.updateLocalPlaylistYouTubePlaylistSyncState( + localPlaylistId, + LocalPlaylistEntity.YouTubeSyncState.Syncing + ) mainRepository.addYouTubePlaylistItem(youtubePlaylistId, videoId).collect { response -> if (response == "STATUS_SUCCEEDED") { - mainRepository.updateLocalPlaylistYouTubePlaylistSyncState(localPlaylistId, LocalPlaylistEntity.YouTubeSyncState.Synced) - Toast.makeText(getApplication(), application.getString(R.string.added_to_youtube_playlist), Toast.LENGTH_SHORT).show() - } - else { - mainRepository.updateLocalPlaylistYouTubePlaylistSyncState(localPlaylistId, LocalPlaylistEntity.YouTubeSyncState.NotSynced) - Toast.makeText(getApplication(), application.getString(R.string.error), Toast.LENGTH_SHORT).show() + mainRepository.updateLocalPlaylistYouTubePlaylistSyncState( + localPlaylistId, + LocalPlaylistEntity.YouTubeSyncState.Synced + ) + Toast.makeText( + getApplication(), + application.getString(R.string.added_to_youtube_playlist), + Toast.LENGTH_SHORT + ).show() + } else { + mainRepository.updateLocalPlaylistYouTubePlaylistSyncState( + localPlaylistId, + LocalPlaylistEntity.YouTubeSyncState.NotSynced + ) + Toast.makeText( + getApplication(), + application.getString(R.string.error), + Toast.LENGTH_SHORT + ).show() } } } @@ -1297,15 +1385,20 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor fun resetRelated() { _related.value = null } + fun getLyricsFromFormat(videoId: String, duration: Int) { viewModelScope.launch { if (dataStoreManager.lyricsProvider.first() == DataStoreManager.MUSIXMATCH) { mainRepository.getSongById(videoId).first().let { song -> - val artist = if (song?.artistName?.firstOrNull() != null && song.artistName.firstOrNull()?.contains("Various Artists") == false) { - song.artistName.firstOrNull() - } else { - simpleMediaServiceHandler?.nowPlaying?.first()?.mediaMetadata?.artist ?: "" - } + val artist = + if (song?.artistName?.firstOrNull() != null && song.artistName.firstOrNull() + ?.contains("Various Artists") == false + ) { + song.artistName.firstOrNull() + } else { + simpleMediaServiceHandler?.nowPlaying?.first()?.mediaMetadata?.artist + ?: "" + } song?.let { if (song.downloadState == DownloadState.STATE_DOWNLOADED) { getSavedLyrics( @@ -1324,7 +1417,11 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor is Resource.Success -> { if (response.second.data != null) { _lyricsProvider.value = LyricsProvider.MUSIXMATCH - insertLyrics(response.second.data!!.toLyricsEntity(videoId)) + insertLyrics( + response.second.data!!.toLyricsEntity( + videoId + ) + ) parseLyrics(response.second.data) if (dataStoreManager.enableTranslateLyric.first() == TRUE) { mainRepository.getTranslateLyrics(response.first) @@ -1372,10 +1469,9 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor } } } - } - else if (dataStoreManager.lyricsProvider.first() == DataStoreManager.YOUTUBE) { - mainRepository.getSongById(videoId).first().let {song -> - mainRepository.getYouTubeCaption(videoId).collect {response -> + } else if (dataStoreManager.lyricsProvider.first() == DataStoreManager.YOUTUBE) { + mainRepository.getSongById(videoId).first().let { song -> + mainRepository.getYouTubeCaption(videoId).collect { response -> _lyrics.value = response when (response) { is Resource.Success -> { @@ -1395,6 +1491,7 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor } } } + is Resource.Error -> { if (_lyrics.value?.message != "reset" && song != null) { if (dataStoreManager.spotifyLyrics.first() == TRUE) { @@ -1563,7 +1660,28 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor fun homeRefreshDone() { _homeRefresh.value = false } + + fun runWorker() { + Log.w("Check Worker", "Worker") + val request = PeriodicWorkRequestBuilder( + 15L, + TimeUnit.MINUTES + ) + .addTag("Worker Test") + .setConstraints( + Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + ) + .build() + WorkManager.getInstance(application).enqueueUniquePeriodicWork( + "Artist Worker", + ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE, + request + ) + } } + sealed class UIEvent { data object PlayPause : UIEvent() data object Backward : UIEvent() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 935ba150..11e46ffe 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -326,4 +326,6 @@ Delete Top videos Trending + New singles + New albums \ No newline at end of file diff --git a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/test/main.kt b/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/test/main.kt index 8e886a29..c76a4e09 100644 --- a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/test/main.kt +++ b/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/test/main.kt @@ -26,8 +26,6 @@ fun main() { runBlocking { YouTube.apply { locale = YouTubeLocale("VN", "vi-VN") - cookie = - "LOGIN_INFO=AFmmF2swRQIhALukjJYCVSmx4H_KV5wySqqqamlJie3XSf88-Okojr4bAiArGSuXl60KhQi7HBIwX6VfTgB_vDESLlwLzHZeax5D9w:QUQ3MjNmd1VSUmRHQm1Gbk05cFJRanR2N1U3R211ZFhva3FFQkJ1M1ZFOXpxV1RxckFIUDZodU1peWVXWnBsZWhCV2tuWnE3ZzJMRlR2Wk9ZNFNGUjNNVjEya3d5cFFxNEZUUXJud2dVV05jZWdqSFNXeDVGcnNCem9FNnhNVU5raWM5bER1bWtJWG1rV3hteWkyVU5wY2xSV2NRSUlsam9n; VISITOR_INFO1_LIVE=eGTToLDprS4; VISITOR_PRIVACY_METADATA=CgJWThIEGgAgHg%3D%3D; SID=g.a000hAiHuGuHH5kVc0x-7aoNYAHFdF_Wwyxdcqc1BGFBFXnpsIoBvYGGsDFYciqX_mutdZS1QgACgYKAdESAQASFQHGX2MiXK9pp3RyG_xxy1fK5_igtBoVAUF8yKoNtfxiaFn6fa7IQob0VpIn0076; __Secure-1PSID=g.a000hAiHuGuHH5kVc0x-7aoNYAHFdF_Wwyxdcqc1BGFBFXnpsIoBEjy2EHK5By9-lRq4UTVTugACgYKAWsSAQASFQHGX2MigdsPkP6Qcp4f_d7WZWpCSxoVAUF8yKrcStxJEW8FafyDOwhj_BR90076; __Secure-3PSID=g.a000hAiHuGuHH5kVc0x-7aoNYAHFdF_Wwyxdcqc1BGFBFXnpsIoBAXrVmNDeYib3Ghrz7kydgwACgYKAWgSAQASFQHGX2MibvzlGfwW2ulcqPYIWLTBoBoVAUF8yKrPBIn1nlRBPVImTPI7dlaW0076; HSID=AGsKQ27qpfujesUvr; SSID=AasGmoCZ1vdXiv1Ha; APISID=rzVnsN-w857cpvG5/Ay85-bZVu69J8lXlZ; SAPISID=8MVesYuDaYm0fkzy/AfbeK9qYrOWRbRsd2; __Secure-1PAPISID=8MVesYuDaYm0fkzy/AfbeK9qYrOWRbRsd2; __Secure-3PAPISID=8MVesYuDaYm0fkzy/AfbeK9qYrOWRbRsd2; NID=512=QwKVkbiWCndYgAcYCm40SazemAog5RNhXlCzXFetpZGko7MP4YP0UM5RiqwWwUezRRPP-4V6Y8L2fyir-rIFPk03yhEv2iiAyhRfLZ5eP8UP6oN5DLyDjPeVtrh7rUo0ScBVT5XZIRyzYo2Tc406pEuEFTnMgmf2EqWaXailKScrPjWBiguIdlu_vjt2W9V6OeQ616Evsb3VBGU; PREF=f6=40000000&tz=Asia.Saigon&f7=100&autoplay=true; __Secure-1PSIDTS=sidts-CjIBYfD7Z-4PA_xWW2TtYReHNKMEp74lRlogaWhmIO5roNFjGeqfTk41S7J_FKAq-H6QShAA; __Secure-3PSIDTS=sidts-CjIBYfD7Z-4PA_xWW2TtYReHNKMEp74lRlogaWhmIO5roNFjGeqfTk41S7J_FKAq-H6QShAA; YSC=vWwzdnDbFZI; ST-qovv0o=csn=MC44MDg1MTk3NDU2MzQxNTQ.&itct=CEcQoLMCGAEiEwj4sKPCx_CEAxWFxjQHHQQWCy8%3D; SIDCC=AKEyXzVgjAYWkEm6KTYOImlUGFOuXMcLL_KB4uazbudJN78P2IC83ZUKHNYbG9HfXZVKys3buKk; __Secure-1PSIDCC=AKEyXzVbsh3dxu0XfpxmI1v-F2AVzJWWOLOutwq5W8FKOc0MK7cbxmL8Sk9ls2VxTQlZAK0OhPk; __Secure-3PSIDCC=AKEyXzXSIAKCMNk766coVdWuFTfEZmK0_Y_BTaQ1FrDRyAMD-hRoump8zi_PJvniPYIWv8vY01M" }.artist( "UC-FbBvrUwI1CmCnurnJlc6Q" ).onSuccess {