diff --git a/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/12.json b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/12.json new file mode 100644 index 00000000..77974597 --- /dev/null +++ b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/12.json @@ -0,0 +1,1151 @@ +{ + "formatVersion": 1, + "database": { + "version": 12, + "identityHash": "2d26471ae15a1cdaf996261b72f81613", + "entities": [ + { + "tableName": "queue", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `track_order` INTEGER NOT NULL, `last_play` INTEGER NOT NULL, `playing_changed` INTEGER NOT NULL, `stream_id` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `sampling_rate` INTEGER, `bit_depth` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`track_order`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "trackOrder", + "columnName": "track_order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastPlay", + "columnName": "last_play", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playingChanged", + "columnName": "playing_changed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "streamId", + "columnName": "stream_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "track", + "columnName": "track", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "genre", + "columnName": "genre", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bitrate", + "columnName": "bitrate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "samplingRate", + "columnName": "sampling_rate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bitDepth", + "columnName": "bit_depth", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isVideo", + "columnName": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "track_order" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "server", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `server_name` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, `address` TEXT NOT NULL, `local_address` TEXT, `timestamp` INTEGER NOT NULL, `low_security` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "localAddress", + "columnName": "local_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLowSecurity", + "columnName": "low_security", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "recent_search", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`search` TEXT NOT NULL, PRIMARY KEY(`search`))", + "fields": [ + { + "fieldPath": "search", + "columnName": "search", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "search" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "download", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlist_id` TEXT, `playlist_name` TEXT, `download_state` INTEGER NOT NULL DEFAULT 1, `download_uri` TEXT DEFAULT '', `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `sampling_rate` INTEGER, `bit_depth` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "playlistId", + "columnName": "playlist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "playlistName", + "columnName": "playlist_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "downloadState", + "columnName": "download_state", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "downloadUri", + "columnName": "download_uri", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "track", + "columnName": "track", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "genre", + "columnName": "genre", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bitrate", + "columnName": "bitrate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "samplingRate", + "columnName": "sampling_rate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bitDepth", + "columnName": "bit_depth", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isVideo", + "columnName": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "chronology", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `server` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `sampling_rate` INTEGER, `bit_depth` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "server", + "columnName": "server", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "track", + "columnName": "track", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "genre", + "columnName": "genre", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bitrate", + "columnName": "bitrate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "samplingRate", + "columnName": "sampling_rate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bitDepth", + "columnName": "bit_depth", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isVideo", + "columnName": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "favorite", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `songId` TEXT, `albumId` TEXT, `artistId` TEXT, `toStar` INTEGER NOT NULL, PRIMARY KEY(`timestamp`))", + "fields": [ + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "albumId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artistId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "toStar", + "columnName": "toStar", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "timestamp" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "session_media_item", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`index` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `id` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, `stream_id` TEXT, `stream_url` TEXT, `timestamp` INTEGER)", + "fields": [ + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "track", + "columnName": "track", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "genre", + "columnName": "genre", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bitrate", + "columnName": "bitrate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isVideo", + "columnName": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "streamId", + "columnName": "stream_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "streamUrl", + "columnName": "stream_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "index" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "playlist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `duration` INTEGER NOT NULL, `coverArt` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "coverArtId", + "columnName": "coverArt", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "lyrics_cache", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`song_id` TEXT NOT NULL, `artist` TEXT, `title` TEXT, `lyrics` TEXT, `structured_lyrics` TEXT, `updated_at` INTEGER NOT NULL, PRIMARY KEY(`song_id`))", + "fields": [ + { + "fieldPath": "songId", + "columnName": "song_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "artist", + "columnName": "artist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lyrics", + "columnName": "lyrics", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "structuredLyrics", + "columnName": "structured_lyrics", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updated_at", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "song_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, '2d26471ae15a1cdaf996261b72f81613')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/database/AppDatabase.java b/app/src/main/java/com/cappielloantonio/tempo/database/AppDatabase.java index b19e934f..3a5e98ef 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/database/AppDatabase.java +++ b/app/src/main/java/com/cappielloantonio/tempo/database/AppDatabase.java @@ -12,6 +12,7 @@ import com.cappielloantonio.tempo.database.converter.DateConverters; import com.cappielloantonio.tempo.database.dao.ChronologyDao; import com.cappielloantonio.tempo.database.dao.DownloadDao; import com.cappielloantonio.tempo.database.dao.FavoriteDao; +import com.cappielloantonio.tempo.database.dao.LyricsDao; import com.cappielloantonio.tempo.database.dao.PlaylistDao; import com.cappielloantonio.tempo.database.dao.QueueDao; import com.cappielloantonio.tempo.database.dao.RecentSearchDao; @@ -20,6 +21,7 @@ import com.cappielloantonio.tempo.database.dao.SessionMediaItemDao; import com.cappielloantonio.tempo.model.Chronology; import com.cappielloantonio.tempo.model.Download; import com.cappielloantonio.tempo.model.Favorite; +import com.cappielloantonio.tempo.model.LyricsCache; import com.cappielloantonio.tempo.model.Queue; import com.cappielloantonio.tempo.model.RecentSearch; import com.cappielloantonio.tempo.model.Server; @@ -28,9 +30,9 @@ import com.cappielloantonio.tempo.subsonic.models.Playlist; @UnstableApi @Database( - version = 11, - entities = {Queue.class, Server.class, RecentSearch.class, Download.class, Chronology.class, Favorite.class, SessionMediaItem.class, Playlist.class}, - autoMigrations = {@AutoMigration(from = 10, to = 11)} + version = 12, + entities = {Queue.class, Server.class, RecentSearch.class, Download.class, Chronology.class, Favorite.class, SessionMediaItem.class, Playlist.class, LyricsCache.class}, + autoMigrations = {@AutoMigration(from = 10, to = 11), @AutoMigration(from = 11, to = 12)} ) @TypeConverters({DateConverters.class}) public abstract class AppDatabase extends RoomDatabase { @@ -62,4 +64,6 @@ public abstract class AppDatabase extends RoomDatabase { public abstract SessionMediaItemDao sessionMediaItemDao(); public abstract PlaylistDao playlistDao(); + + public abstract LyricsDao lyricsDao(); } diff --git a/app/src/main/java/com/cappielloantonio/tempo/database/dao/LyricsDao.java b/app/src/main/java/com/cappielloantonio/tempo/database/dao/LyricsDao.java new file mode 100644 index 00000000..89d0d585 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/database/dao/LyricsDao.java @@ -0,0 +1,24 @@ +package com.cappielloantonio.tempo.database.dao; + +import androidx.lifecycle.LiveData; +import androidx.room.Dao; +import androidx.room.Insert; +import androidx.room.OnConflictStrategy; +import androidx.room.Query; + +import com.cappielloantonio.tempo.model.LyricsCache; + +@Dao +public interface LyricsDao { + @Query("SELECT * FROM lyrics_cache WHERE song_id = :songId") + LyricsCache getOne(String songId); + + @Query("SELECT * FROM lyrics_cache WHERE song_id = :songId") + LiveData observeOne(String songId); + + @Insert(onConflict = OnConflictStrategy.REPLACE) + void insert(LyricsCache lyricsCache); + + @Query("DELETE FROM lyrics_cache WHERE song_id = :songId") + void delete(String songId); +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/model/LyricsCache.kt b/app/src/main/java/com/cappielloantonio/tempo/model/LyricsCache.kt new file mode 100644 index 00000000..3c437e2c --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/model/LyricsCache.kt @@ -0,0 +1,25 @@ +package com.cappielloantonio.tempo.model + +import androidx.annotation.Keep +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import kotlin.jvm.JvmOverloads + +@Keep +@Entity(tableName = "lyrics_cache") +data class LyricsCache @JvmOverloads constructor( + @PrimaryKey + @ColumnInfo(name = "song_id") + var songId: String, + @ColumnInfo(name = "artist") + var artist: String? = null, + @ColumnInfo(name = "title") + var title: String? = null, + @ColumnInfo(name = "lyrics") + var lyrics: String? = null, + @ColumnInfo(name = "structured_lyrics") + var structuredLyrics: String? = null, + @ColumnInfo(name = "updated_at") + var updatedAt: Long = System.currentTimeMillis() +) \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/LyricsRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/LyricsRepository.java new file mode 100644 index 00000000..fb7a05a3 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/LyricsRepository.java @@ -0,0 +1,92 @@ +package com.cappielloantonio.tempo.repository; + +import androidx.lifecycle.LiveData; + +import com.cappielloantonio.tempo.database.AppDatabase; +import com.cappielloantonio.tempo.database.dao.LyricsDao; +import com.cappielloantonio.tempo.model.LyricsCache; + +public class LyricsRepository { + private final LyricsDao lyricsDao = AppDatabase.getInstance().lyricsDao(); + + public LyricsCache getLyrics(String songId) { + GetLyricsThreadSafe getLyricsThreadSafe = new GetLyricsThreadSafe(lyricsDao, songId); + Thread thread = new Thread(getLyricsThreadSafe); + thread.start(); + + try { + thread.join(); + return getLyricsThreadSafe.getLyrics(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + return null; + } + + public LiveData observeLyrics(String songId) { + return lyricsDao.observeOne(songId); + } + + public void insert(LyricsCache lyricsCache) { + InsertThreadSafe insert = new InsertThreadSafe(lyricsDao, lyricsCache); + Thread thread = new Thread(insert); + thread.start(); + } + + public void delete(String songId) { + DeleteThreadSafe delete = new DeleteThreadSafe(lyricsDao, songId); + Thread thread = new Thread(delete); + thread.start(); + } + + private static class GetLyricsThreadSafe implements Runnable { + private final LyricsDao lyricsDao; + private final String songId; + private LyricsCache lyricsCache; + + public GetLyricsThreadSafe(LyricsDao lyricsDao, String songId) { + this.lyricsDao = lyricsDao; + this.songId = songId; + } + + @Override + public void run() { + lyricsCache = lyricsDao.getOne(songId); + } + + public LyricsCache getLyrics() { + return lyricsCache; + } + } + + private static class InsertThreadSafe implements Runnable { + private final LyricsDao lyricsDao; + private final LyricsCache lyricsCache; + + public InsertThreadSafe(LyricsDao lyricsDao, LyricsCache lyricsCache) { + this.lyricsDao = lyricsDao; + this.lyricsCache = lyricsCache; + } + + @Override + public void run() { + lyricsDao.insert(lyricsCache); + } + } + + private static class DeleteThreadSafe implements Runnable { + private final LyricsDao lyricsDao; + private final String songId; + + public DeleteThreadSafe(LyricsDao lyricsDao, String songId) { + this.lyricsDao = lyricsDao; + this.songId = songId; + } + + @Override + public void run() { + lyricsDao.delete(songId); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerLyricsFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerLyricsFragment.java index 7140632f..24a1abcd 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerLyricsFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerLyricsFragment.java @@ -4,15 +4,16 @@ import android.annotation.SuppressLint; import android.content.ComponentName; import android.os.Bundle; import android.os.Handler; +import android.text.Layout; import android.text.Spannable; import android.text.SpannableString; -import android.text.Layout; +import android.text.TextUtils; import android.text.style.ForegroundColorSpan; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -29,10 +30,10 @@ import com.cappielloantonio.tempo.service.MediaService; import com.cappielloantonio.tempo.subsonic.models.Line; import com.cappielloantonio.tempo.subsonic.models.LyricsList; import com.cappielloantonio.tempo.util.MusicUtil; -import com.cappielloantonio.tempo.util.OpenSubsonicExtensionsUtil; import com.cappielloantonio.tempo.util.Preferences; import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel; import com.google.common.util.concurrent.ListenableFuture; +import com.google.android.material.button.MaterialButton; import com.google.common.util.concurrent.MoreExecutors; import java.util.List; @@ -48,6 +49,9 @@ public class PlayerLyricsFragment extends Fragment { private MediaBrowser mediaBrowser; private Handler syncLyricsHandler; private Runnable syncLyricsRunnable; + private String currentLyrics; + private LyricsList currentLyricsList; + private String currentDescription; @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -66,6 +70,7 @@ public class PlayerLyricsFragment extends Fragment { super.onViewCreated(view, savedInstanceState); initPanelContent(); + observeDownloadState(); } @Override @@ -101,12 +106,26 @@ public class PlayerLyricsFragment extends Fragment { public void onDestroyView() { super.onDestroyView(); bind = null; + currentLyrics = null; + currentLyricsList = null; + currentDescription = null; } private void initOverlay() { bind.syncLyricsTapButton.setOnClickListener(view -> { playerBottomSheetViewModel.changeSyncLyricsState(); }); + + bind.downloadLyricsButton.setOnClickListener(view -> { + boolean saved = playerBottomSheetViewModel.downloadCurrentLyrics(); + if (getContext() != null) { + Toast.makeText( + requireContext(), + saved ? R.string.player_lyrics_download_success : R.string.player_lyrics_download_failure, + Toast.LENGTH_SHORT + ).show(); + } + }); } private void initializeBrowser() { @@ -136,50 +155,91 @@ public class PlayerLyricsFragment extends Fragment { } private void initPanelContent() { - if (OpenSubsonicExtensionsUtil.isSongLyricsExtensionAvailable()) { - playerBottomSheetViewModel.getLiveLyricsList().observe(getViewLifecycleOwner(), lyricsList -> { - setPanelContent(null, lyricsList); - }); - } else { - playerBottomSheetViewModel.getLiveLyrics().observe(getViewLifecycleOwner(), lyrics -> { - setPanelContent(lyrics, null); - }); - } + playerBottomSheetViewModel.getLiveLyrics().observe(getViewLifecycleOwner(), lyrics -> { + currentLyrics = lyrics; + updatePanelContent(); + }); + + playerBottomSheetViewModel.getLiveLyricsList().observe(getViewLifecycleOwner(), lyricsList -> { + currentLyricsList = lyricsList; + updatePanelContent(); + }); + + playerBottomSheetViewModel.getLiveDescription().observe(getViewLifecycleOwner(), description -> { + currentDescription = description; + updatePanelContent(); + }); } - private void setPanelContent(String lyrics, LyricsList lyricsList) { - playerBottomSheetViewModel.getLiveDescription().observe(getViewLifecycleOwner(), description -> { + private void observeDownloadState() { + playerBottomSheetViewModel.getLyricsCachedState().observe(getViewLifecycleOwner(), cached -> { if (bind != null) { - bind.nowPlayingSongLyricsSrollView.smoothScrollTo(0, 0); - - if (lyrics != null && !lyrics.trim().equals("")) { - bind.nowPlayingSongLyricsTextView.setText(MusicUtil.getReadableLyrics(lyrics)); - bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE); - bind.emptyDescriptionImageView.setVisibility(View.GONE); - bind.titleEmptyDescriptionLabel.setVisibility(View.GONE); - bind.syncLyricsTapButton.setVisibility(View.GONE); - } else if (lyricsList != null && lyricsList.getStructuredLyrics() != null) { - setSyncLirics(lyricsList); - bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE); - bind.emptyDescriptionImageView.setVisibility(View.GONE); - bind.titleEmptyDescriptionLabel.setVisibility(View.GONE); - bind.syncLyricsTapButton.setVisibility(View.VISIBLE); - } else if (description != null && !description.trim().equals("")) { - bind.nowPlayingSongLyricsTextView.setText(MusicUtil.getReadableLyrics(description)); - bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE); - bind.emptyDescriptionImageView.setVisibility(View.GONE); - bind.titleEmptyDescriptionLabel.setVisibility(View.GONE); - bind.syncLyricsTapButton.setVisibility(View.GONE); + MaterialButton downloadButton = (MaterialButton) bind.downloadLyricsButton; + if (cached != null && cached) { + downloadButton.setIconResource(R.drawable.ic_done); + downloadButton.setContentDescription(getString(R.string.player_lyrics_downloaded_content_description)); } else { - bind.nowPlayingSongLyricsTextView.setVisibility(View.GONE); - bind.emptyDescriptionImageView.setVisibility(View.VISIBLE); - bind.titleEmptyDescriptionLabel.setVisibility(View.VISIBLE); - bind.syncLyricsTapButton.setVisibility(View.GONE); + downloadButton.setIconResource(R.drawable.ic_download); + downloadButton.setContentDescription(getString(R.string.player_lyrics_download_content_description)); } } }); } + private void updatePanelContent() { + if (bind == null) { + return; + } + + bind.nowPlayingSongLyricsSrollView.smoothScrollTo(0, 0); + + if (hasStructuredLyrics(currentLyricsList)) { + setSyncLirics(currentLyricsList); + bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE); + bind.emptyDescriptionImageView.setVisibility(View.GONE); + bind.titleEmptyDescriptionLabel.setVisibility(View.GONE); + bind.syncLyricsTapButton.setVisibility(View.VISIBLE); + bind.downloadLyricsButton.setVisibility(View.VISIBLE); + bind.downloadLyricsButton.setEnabled(true); + } else if (hasText(currentLyrics)) { + bind.nowPlayingSongLyricsTextView.setText(MusicUtil.getReadableLyrics(currentLyrics)); + bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE); + bind.emptyDescriptionImageView.setVisibility(View.GONE); + bind.titleEmptyDescriptionLabel.setVisibility(View.GONE); + bind.syncLyricsTapButton.setVisibility(View.GONE); + bind.downloadLyricsButton.setVisibility(View.VISIBLE); + bind.downloadLyricsButton.setEnabled(true); + } else if (hasText(currentDescription)) { + bind.nowPlayingSongLyricsTextView.setText(MusicUtil.getReadableLyrics(currentDescription)); + bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE); + bind.emptyDescriptionImageView.setVisibility(View.GONE); + bind.titleEmptyDescriptionLabel.setVisibility(View.GONE); + bind.syncLyricsTapButton.setVisibility(View.GONE); + bind.downloadLyricsButton.setVisibility(View.GONE); + bind.downloadLyricsButton.setEnabled(false); + } else { + bind.nowPlayingSongLyricsTextView.setVisibility(View.GONE); + bind.emptyDescriptionImageView.setVisibility(View.VISIBLE); + bind.titleEmptyDescriptionLabel.setVisibility(View.VISIBLE); + bind.syncLyricsTapButton.setVisibility(View.GONE); + bind.downloadLyricsButton.setVisibility(View.GONE); + bind.downloadLyricsButton.setEnabled(false); + } + } + + private boolean hasText(String value) { + return value != null && !value.trim().isEmpty(); + } + + private boolean hasStructuredLyrics(LyricsList lyricsList) { + return lyricsList != null + && lyricsList.getStructuredLyrics() != null + && !lyricsList.getStructuredLyrics().isEmpty() + && lyricsList.getStructuredLyrics().get(0) != null + && lyricsList.getStructuredLyrics().get(0).getLine() != null + && !lyricsList.getStructuredLyrics().get(0).getLine().isEmpty(); + } + @SuppressLint("DefaultLocale") private void setSyncLirics(LyricsList lyricsList) { if (lyricsList.getStructuredLyrics() != null && !lyricsList.getStructuredLyrics().isEmpty() && lyricsList.getStructuredLyrics().get(0).getLine() != null) { @@ -198,28 +258,28 @@ public class PlayerLyricsFragment extends Fragment { private void defineProgressHandler() { playerBottomSheetViewModel.getLiveLyricsList().observe(getViewLifecycleOwner(), lyricsList -> { - if (lyricsList != null) { - - if (lyricsList.getStructuredLyrics() != null && lyricsList.getStructuredLyrics().get(0) != null && !lyricsList.getStructuredLyrics().get(0).getSynced()) { - releaseHandler(); - return; - } - - syncLyricsHandler = new Handler(); - syncLyricsRunnable = () -> { - if (syncLyricsHandler != null) { - if (bind != null) { - displaySyncedLyrics(); - } - - syncLyricsHandler.postDelayed(syncLyricsRunnable, 250); - } - }; - - syncLyricsHandler.postDelayed(syncLyricsRunnable, 250); - } else { + if (!hasStructuredLyrics(lyricsList)) { releaseHandler(); + return; } + + if (!lyricsList.getStructuredLyrics().get(0).getSynced()) { + releaseHandler(); + return; + } + + syncLyricsHandler = new Handler(); + syncLyricsRunnable = () -> { + if (syncLyricsHandler != null) { + if (bind != null) { + displaySyncedLyrics(); + } + + syncLyricsHandler.postDelayed(syncLyricsRunnable, 250); + } + }; + + syncLyricsHandler.postDelayed(syncLyricsRunnable, 250); }); } @@ -227,7 +287,7 @@ public class PlayerLyricsFragment extends Fragment { LyricsList lyricsList = playerBottomSheetViewModel.getLiveLyricsList().getValue(); int timestamp = (int) (mediaBrowser.getCurrentPosition()); - if (lyricsList != null && lyricsList.getStructuredLyrics() != null && !lyricsList.getStructuredLyrics().isEmpty() && lyricsList.getStructuredLyrics().get(0).getLine() != null) { + if (hasStructuredLyrics(lyricsList)) { StringBuilder lyricsBuilder = new StringBuilder(); List lines = lyricsList.getStructuredLyrics().get(0).getLine(); diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java index 602acc8c..ef4f2134 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java @@ -116,6 +116,7 @@ public class SettingsFragment extends PreferenceFragmentCompat { actionChangeDownloadStorage(); actionDeleteDownloadStorage(); actionKeepScreenOn(); + actionAutoDownloadLyrics(); bindMediaService(); actionAppEqualizer(); @@ -357,6 +358,21 @@ public class SettingsFragment extends PreferenceFragmentCompat { }); } + private void actionAutoDownloadLyrics() { + SwitchPreference preference = findPreference("auto_download_lyrics"); + if (preference == null) { + return; + } + + preference.setChecked(Preferences.isAutoDownloadLyricsEnabled()); + preference.setOnPreferenceChangeListener((pref, newValue) -> { + if (newValue instanceof Boolean) { + Preferences.setAutoDownloadLyricsEnabled((Boolean) newValue); + } + return true; + }); + } + private void getScanStatus() { settingViewModel.getScanStatus(new ScanCallback() { @Override diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt b/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt index f4188719..80276319 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt @@ -46,6 +46,7 @@ object Preferences { private const val ROUNDED_CORNER_SIZE = "rounded_corner_size" private const val PODCAST_SECTION_VISIBILITY = "podcast_section_visibility" private const val RADIO_SECTION_VISIBILITY = "radio_section_visibility" + private const val AUTO_DOWNLOAD_LYRICS = "auto_download_lyrics" private const val MUSIC_DIRECTORY_SECTION_VISIBILITY = "music_directory_section_visibility" private const val REPLAY_GAIN_MODE = "replay_gain_mode" private const val AUDIO_TRANSCODE_PRIORITY = "audio_transcode_priority" @@ -163,6 +164,24 @@ object Preferences { App.getInstance().preferences.edit().putString(OPEN_SUBSONIC_EXTENSIONS, Gson().toJson(extension)).apply() } + @JvmStatic + fun isAutoDownloadLyricsEnabled(): Boolean { + val preferences = App.getInstance().preferences + + if (preferences.contains(AUTO_DOWNLOAD_LYRICS)) { + return preferences.getBoolean(AUTO_DOWNLOAD_LYRICS, false) + } + + return false + } + + @JvmStatic + fun setAutoDownloadLyricsEnabled(isEnabled: Boolean) { + App.getInstance().preferences.edit() + .putBoolean(AUTO_DOWNLOAD_LYRICS, isEnabled) + .apply() + } + @JvmStatic fun getLocalAddress(): String? { return App.getInstance().preferences.getString(LOCAL_ADDRESS, null) diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlayerBottomSheetViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlayerBottomSheetViewModel.java index bf90fa65..6f972add 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlayerBottomSheetViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlayerBottomSheetViewModel.java @@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.viewmodel; import android.app.Application; import android.content.Context; +import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.OptIn; @@ -9,14 +10,17 @@ import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.Observer; import androidx.media3.common.util.UnstableApi; import com.cappielloantonio.tempo.interfaces.StarCallback; import com.cappielloantonio.tempo.model.Download; +import com.cappielloantonio.tempo.model.LyricsCache; import com.cappielloantonio.tempo.model.Queue; import com.cappielloantonio.tempo.repository.AlbumRepository; import com.cappielloantonio.tempo.repository.ArtistRepository; import com.cappielloantonio.tempo.repository.FavoriteRepository; +import com.cappielloantonio.tempo.repository.LyricsRepository; import com.cappielloantonio.tempo.repository.OpenRepository; import com.cappielloantonio.tempo.repository.QueueRepository; import com.cappielloantonio.tempo.repository.SongRepository; @@ -31,6 +35,7 @@ import com.cappielloantonio.tempo.util.MappingUtil; import com.cappielloantonio.tempo.util.NetworkUtil; import com.cappielloantonio.tempo.util.OpenSubsonicExtensionsUtil; import com.cappielloantonio.tempo.util.Preferences; +import com.google.gson.Gson; import java.util.Collections; import java.util.Date; @@ -47,14 +52,20 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel { private final QueueRepository queueRepository; private final FavoriteRepository favoriteRepository; private final OpenRepository openRepository; + private final LyricsRepository lyricsRepository; private final MutableLiveData lyricsLiveData = new MutableLiveData<>(null); private final MutableLiveData lyricsListLiveData = new MutableLiveData<>(null); + private final MutableLiveData lyricsCachedLiveData = new MutableLiveData<>(false); private final MutableLiveData descriptionLiveData = new MutableLiveData<>(null); private final MutableLiveData liveMedia = new MutableLiveData<>(null); private final MutableLiveData liveAlbum = new MutableLiveData<>(null); private final MutableLiveData liveArtist = new MutableLiveData<>(null); private final MutableLiveData> instantMix = new MutableLiveData<>(null); + private final Gson gson = new Gson(); private boolean lyricsSyncState = true; + private LiveData cachedLyricsSource; + private String currentSongId; + private final Observer cachedLyricsObserver = this::onCachedLyricsChanged; public PlayerBottomSheetViewModel(@NonNull Application application) { @@ -66,6 +77,7 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel { queueRepository = new QueueRepository(); favoriteRepository = new FavoriteRepository(); openRepository = new OpenRepository(); + lyricsRepository = new LyricsRepository(); } public LiveData> getQueueSong() { @@ -139,12 +151,49 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel { } public void refreshMediaInfo(LifecycleOwner owner, Child media) { + lyricsLiveData.postValue(null); + lyricsListLiveData.postValue(null); + lyricsCachedLiveData.postValue(false); + + clearCachedLyricsObserver(); + + String songId = media != null ? media.getId() : currentSongId; + + if (TextUtils.isEmpty(songId) || owner == null) { + return; + } + + currentSongId = songId; + + observeCachedLyrics(owner, songId); + + LyricsCache cachedLyrics = lyricsRepository.getLyrics(songId); + if (cachedLyrics != null) { + onCachedLyricsChanged(cachedLyrics); + } + + if (NetworkUtil.isOffline() || media == null) { + return; + } + if (OpenSubsonicExtensionsUtil.isSongLyricsExtensionAvailable()) { - openRepository.getLyricsBySongId(media.getId()).observe(owner, lyricsListLiveData::postValue); - lyricsLiveData.postValue(null); + openRepository.getLyricsBySongId(media.getId()).observe(owner, lyricsList -> { + lyricsListLiveData.postValue(lyricsList); + lyricsLiveData.postValue(null); + + if (shouldAutoDownloadLyrics() && hasStructuredLyrics(lyricsList)) { + saveLyricsToCache(media, null, lyricsList); + } + }); } else { - songRepository.getSongLyrics(media).observe(owner, lyricsLiveData::postValue); - lyricsListLiveData.postValue(null); + songRepository.getSongLyrics(media).observe(owner, lyrics -> { + lyricsLiveData.postValue(lyrics); + lyricsListLiveData.postValue(null); + + if (shouldAutoDownloadLyrics() && !TextUtils.isEmpty(lyrics)) { + saveLyricsToCache(media, lyrics, null); + } + }); } } @@ -153,6 +202,17 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel { } public void setLiveMedia(LifecycleOwner owner, String mediaType, String mediaId) { + currentSongId = mediaId; + + if (!TextUtils.isEmpty(mediaId)) { + refreshMediaInfo(owner, null); + } else { + clearCachedLyricsObserver(); + lyricsLiveData.postValue(null); + lyricsListLiveData.postValue(null); + lyricsCachedLiveData.postValue(false); + } + if (mediaType != null) { switch (mediaType) { case Constants.MEDIA_TYPE_MUSIC: @@ -162,7 +222,12 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel { case Constants.MEDIA_TYPE_PODCAST: liveMedia.postValue(null); break; + default: + liveMedia.postValue(null); + break; } + } else { + liveMedia.postValue(null); } } @@ -233,6 +298,105 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel { return false; } + private void observeCachedLyrics(LifecycleOwner owner, String songId) { + if (TextUtils.isEmpty(songId)) { + return; + } + + cachedLyricsSource = lyricsRepository.observeLyrics(songId); + cachedLyricsSource.observe(owner, cachedLyricsObserver); + } + + private void clearCachedLyricsObserver() { + if (cachedLyricsSource != null) { + cachedLyricsSource.removeObserver(cachedLyricsObserver); + cachedLyricsSource = null; + } + } + + private void onCachedLyricsChanged(LyricsCache lyricsCache) { + if (lyricsCache == null) { + lyricsCachedLiveData.postValue(false); + return; + } + + lyricsCachedLiveData.postValue(true); + + if (!TextUtils.isEmpty(lyricsCache.getStructuredLyrics())) { + try { + LyricsList cachedList = gson.fromJson(lyricsCache.getStructuredLyrics(), LyricsList.class); + lyricsListLiveData.postValue(cachedList); + lyricsLiveData.postValue(null); + } catch (Exception exception) { + lyricsListLiveData.postValue(null); + lyricsLiveData.postValue(lyricsCache.getLyrics()); + } + } else { + lyricsListLiveData.postValue(null); + lyricsLiveData.postValue(lyricsCache.getLyrics()); + } + } + + private void saveLyricsToCache(Child media, String lyrics, LyricsList lyricsList) { + if (media == null) { + return; + } + + if ((lyricsList == null || !hasStructuredLyrics(lyricsList)) && TextUtils.isEmpty(lyrics)) { + return; + } + + LyricsCache lyricsCache = new LyricsCache(media.getId()); + lyricsCache.setArtist(media.getArtist()); + lyricsCache.setTitle(media.getTitle()); + lyricsCache.setUpdatedAt(System.currentTimeMillis()); + + if (lyricsList != null && hasStructuredLyrics(lyricsList)) { + lyricsCache.setStructuredLyrics(gson.toJson(lyricsList)); + lyricsCache.setLyrics(null); + } else { + lyricsCache.setLyrics(lyrics); + lyricsCache.setStructuredLyrics(null); + } + + lyricsRepository.insert(lyricsCache); + lyricsCachedLiveData.postValue(true); + } + + private boolean hasStructuredLyrics(LyricsList lyricsList) { + return lyricsList != null + && lyricsList.getStructuredLyrics() != null + && !lyricsList.getStructuredLyrics().isEmpty() + && lyricsList.getStructuredLyrics().get(0) != null + && lyricsList.getStructuredLyrics().get(0).getLine() != null + && !lyricsList.getStructuredLyrics().get(0).getLine().isEmpty(); + } + + private boolean shouldAutoDownloadLyrics() { + return Preferences.isAutoDownloadLyricsEnabled(); + } + + public boolean downloadCurrentLyrics() { + Child media = getLiveMedia().getValue(); + if (media == null) { + return false; + } + + LyricsList lyricsList = lyricsListLiveData.getValue(); + String lyrics = lyricsLiveData.getValue(); + + if ((lyricsList == null || !hasStructuredLyrics(lyricsList)) && TextUtils.isEmpty(lyrics)) { + return false; + } + + saveLyricsToCache(media, lyrics, lyricsList); + return true; + } + + public LiveData getLyricsCachedState() { + return lyricsCachedLiveData; + } + public void changeSyncLyricsState() { lyricsSyncState = !lyricsSyncState; } diff --git a/app/src/main/res/layout/inner_fragment_player_lyrics.xml b/app/src/main/res/layout/inner_fragment_player_lyrics.xml index f9b5ba48..5ece8996 100644 --- a/app/src/main/res/layout/inner_fragment_player_lyrics.xml +++ b/app/src/main/res/layout/inner_fragment_player_lyrics.xml @@ -51,7 +51,25 @@ app:layout_constraintTop_toTopOf="parent" /> -