From e8ab5aacc7b454b1cdb8bc75fcbcf3628e870c70 Mon Sep 17 00:00:00 2001 From: Mauricio Colli Date: Sun, 28 Apr 2019 17:43:52 -0300 Subject: [PATCH] Setup initial database for feed implementation - Update the database diagram - Add new migration for the new tables and fields - Enable schema exports --- app/build.gradle | 6 + .../2.json | 479 +++++++++++++ .../3.json | 647 ++++++++++++++++++ .../org/schabi/newpipe/NewPipeDatabase.java | 5 +- .../schabi/newpipe/database/AppDatabase.java | 24 +- .../schabi/newpipe/database/Migrations.java | 33 +- .../newpipe/database/feed/dao/FeedDAO.kt | 47 ++ .../newpipe/database/feed/dao/FeedGroupDAO.kt | 17 + .../newpipe/database/feed/model/FeedEntity.kt | 43 ++ .../database/feed/model/FeedGroupEntity.kt | 27 + .../feed/model/FeedGroupSubscriptionEntity.kt | 45 ++ .../history/model/StreamHistoryEntry.java | 59 -- .../history/model/StreamHistoryEntry.kt | 30 + .../playlist/PlaylistStreamEntry.java | 60 -- .../database/playlist/PlaylistStreamEntry.kt | 34 + .../stream/StreamStatisticsEntry.java | 69 -- .../database/stream/StreamStatisticsEntry.kt | 42 ++ .../database/stream/dao/StreamDAO.java | 98 --- .../newpipe/database/stream/dao/StreamDAO.kt | 116 ++++ .../database/stream/model/StreamEntity.java | 153 ----- .../database/stream/model/StreamEntity.kt | 107 +++ .../subscription/SubscriptionDAO.java | 3 + .../subscription/SubscriptionEntity.java | 2 +- .../local/history/HistoryRecordManager.java | 4 +- .../history/StatisticsPlaylistFragment.java | 12 +- .../holder/LocalPlaylistStreamItemHolder.java | 18 +- .../LocalStatisticStreamItemHolder.java | 22 +- .../local/playlist/LocalPlaylistFragment.java | 10 +- assets/db.dia | Bin 2508 -> 2880 bytes 29 files changed, 1722 insertions(+), 490 deletions(-) create mode 100644 app/schemas/org.schabi.newpipe.database.AppDatabase/2.json create mode 100644 app/schemas/org.schabi.newpipe.database.AppDatabase/3.json create mode 100644 app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt create mode 100644 app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedGroupDAO.kt create mode 100644 app/src/main/java/org/schabi/newpipe/database/feed/model/FeedEntity.kt create mode 100644 app/src/main/java/org/schabi/newpipe/database/feed/model/FeedGroupEntity.kt create mode 100644 app/src/main/java/org/schabi/newpipe/database/feed/model/FeedGroupSubscriptionEntity.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntry.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntry.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.kt diff --git a/app/build.gradle b/app/build.gradle index b4df55d4e..3d4d82d97 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -35,6 +35,12 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true + + javaCompileOptions { + annotationProcessorOptions { + arguments = ["room.schemaLocation": "$projectDir/schemas".toString()] + } + } } buildTypes { diff --git a/app/schemas/org.schabi.newpipe.database.AppDatabase/2.json b/app/schemas/org.schabi.newpipe.database.AppDatabase/2.json new file mode 100644 index 000000000..2532e330e --- /dev/null +++ b/app/schemas/org.schabi.newpipe.database.AppDatabase/2.json @@ -0,0 +1,479 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "b7856223e2595ddf20a3ce6243ce9527", + "entities": [ + { + "tableName": "subscriptions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `name` TEXT, `avatar_url` TEXT, `subscriber_count` INTEGER, `description` TEXT)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "service_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarUrl", + "columnName": "avatar_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "subscriberCount", + "columnName": "subscriber_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_subscriptions_service_id_url", + "unique": true, + "columnNames": [ + "service_id", + "url" + ], + "createSql": "CREATE UNIQUE INDEX `index_subscriptions_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "search_history", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `creation_date` INTEGER, `service_id` INTEGER NOT NULL, `search` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "creationDate", + "columnName": "creation_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "serviceId", + "columnName": "service_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "search", + "columnName": "search", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_search_history_search", + "unique": false, + "columnNames": [ + "search" + ], + "createSql": "CREATE INDEX `index_search_history_search` ON `${TABLE_NAME}` (`search`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "streams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, `stream_type` TEXT, `duration` INTEGER, `uploader` TEXT, `thumbnail_url` TEXT)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "service_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "streamType", + "columnName": "stream_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "uploader", + "columnName": "uploader", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnail_url", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_streams_service_id_url", + "unique": true, + "columnNames": [ + "service_id", + "url" + ], + "createSql": "CREATE UNIQUE INDEX `index_streams_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "stream_history", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, `repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "streamUid", + "columnName": "stream_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accessDate", + "columnName": "access_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repeatCount", + "columnName": "repeat_count", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "stream_id", + "access_date" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_stream_history_stream_id", + "unique": false, + "columnNames": [ + "stream_id" + ], + "createSql": "CREATE INDEX `index_stream_history_stream_id` ON `${TABLE_NAME}` (`stream_id`)" + } + ], + "foreignKeys": [ + { + "table": "streams", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "stream_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "stream_state", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "streamUid", + "columnName": "stream_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progressTime", + "columnName": "progress_time", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "stream_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "streams", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "stream_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "playlists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `thumbnail_url` TEXT)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnail_url", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_playlists_name", + "unique": false, + "columnNames": [ + "name" + ], + "createSql": "CREATE INDEX `index_playlists_name` ON `${TABLE_NAME}` (`name`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "playlist_stream_join", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "playlistUid", + "columnName": "playlist_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "streamUid", + "columnName": "stream_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "join_index", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "playlist_id", + "join_index" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_playlist_stream_join_playlist_id_join_index", + "unique": true, + "columnNames": [ + "playlist_id", + "join_index" + ], + "createSql": "CREATE UNIQUE INDEX `index_playlist_stream_join_playlist_id_join_index` ON `${TABLE_NAME}` (`playlist_id`, `join_index`)" + }, + { + "name": "index_playlist_stream_join_stream_id", + "unique": false, + "columnNames": [ + "stream_id" + ], + "createSql": "CREATE INDEX `index_playlist_stream_join_stream_id` ON `${TABLE_NAME}` (`stream_id`)" + } + ], + "foreignKeys": [ + { + "table": "playlists", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "playlist_id" + ], + "referencedColumns": [ + "uid" + ] + }, + { + "table": "streams", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "stream_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "remote_playlists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "service_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnail_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uploader", + "columnName": "uploader", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "streamCount", + "columnName": "stream_count", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_remote_playlists_name", + "unique": false, + "columnNames": [ + "name" + ], + "createSql": "CREATE INDEX `index_remote_playlists_name` ON `${TABLE_NAME}` (`name`)" + }, + { + "name": "index_remote_playlists_service_id_url", + "unique": true, + "columnNames": [ + "service_id", + "url" + ], + "createSql": "CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)" + } + ], + "foreignKeys": [] + } + ], + "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, \"b7856223e2595ddf20a3ce6243ce9527\")" + ] + } +} \ No newline at end of file diff --git a/app/schemas/org.schabi.newpipe.database.AppDatabase/3.json b/app/schemas/org.schabi.newpipe.database.AppDatabase/3.json new file mode 100644 index 000000000..fdfc3740e --- /dev/null +++ b/app/schemas/org.schabi.newpipe.database.AppDatabase/3.json @@ -0,0 +1,647 @@ +{ + "formatVersion": 1, + "database": { + "version": 3, + "identityHash": "ecffbb2ea251aeb38a8f508acf2aa404", + "entities": [ + { + "tableName": "subscriptions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `name` TEXT, `avatar_url` TEXT, `subscriber_count` INTEGER, `description` TEXT)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "service_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarUrl", + "columnName": "avatar_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "subscriberCount", + "columnName": "subscriber_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_subscriptions_service_id_url", + "unique": true, + "columnNames": [ + "service_id", + "url" + ], + "createSql": "CREATE UNIQUE INDEX `index_subscriptions_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "search_history", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `creation_date` INTEGER, `service_id` INTEGER NOT NULL, `search` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "creationDate", + "columnName": "creation_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "serviceId", + "columnName": "service_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "search", + "columnName": "search", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_search_history_search", + "unique": false, + "columnNames": [ + "search" + ], + "createSql": "CREATE INDEX `index_search_history_search` ON `${TABLE_NAME}` (`search`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "streams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `stream_type` TEXT NOT NULL, `duration` INTEGER NOT NULL, `uploader` TEXT NOT NULL, `thumbnail_url` TEXT, `view_count` INTEGER, `textual_upload_date` TEXT, `upload_date` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "service_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "streamType", + "columnName": "stream_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uploader", + "columnName": "uploader", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnail_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "viewCount", + "columnName": "view_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "textualUploadDate", + "columnName": "textual_upload_date", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uploadDate", + "columnName": "upload_date", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_streams_service_id_url", + "unique": true, + "columnNames": [ + "service_id", + "url" + ], + "createSql": "CREATE UNIQUE INDEX `index_streams_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "stream_history", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, `repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "streamUid", + "columnName": "stream_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accessDate", + "columnName": "access_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repeatCount", + "columnName": "repeat_count", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "stream_id", + "access_date" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_stream_history_stream_id", + "unique": false, + "columnNames": [ + "stream_id" + ], + "createSql": "CREATE INDEX `index_stream_history_stream_id` ON `${TABLE_NAME}` (`stream_id`)" + } + ], + "foreignKeys": [ + { + "table": "streams", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "stream_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "stream_state", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "streamUid", + "columnName": "stream_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progressTime", + "columnName": "progress_time", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "stream_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "streams", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "stream_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "playlists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `thumbnail_url` TEXT)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnail_url", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_playlists_name", + "unique": false, + "columnNames": [ + "name" + ], + "createSql": "CREATE INDEX `index_playlists_name` ON `${TABLE_NAME}` (`name`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "playlist_stream_join", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "playlistUid", + "columnName": "playlist_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "streamUid", + "columnName": "stream_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "join_index", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "playlist_id", + "join_index" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_playlist_stream_join_playlist_id_join_index", + "unique": true, + "columnNames": [ + "playlist_id", + "join_index" + ], + "createSql": "CREATE UNIQUE INDEX `index_playlist_stream_join_playlist_id_join_index` ON `${TABLE_NAME}` (`playlist_id`, `join_index`)" + }, + { + "name": "index_playlist_stream_join_stream_id", + "unique": false, + "columnNames": [ + "stream_id" + ], + "createSql": "CREATE INDEX `index_playlist_stream_join_stream_id` ON `${TABLE_NAME}` (`stream_id`)" + } + ], + "foreignKeys": [ + { + "table": "playlists", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "playlist_id" + ], + "referencedColumns": [ + "uid" + ] + }, + { + "table": "streams", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "stream_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "remote_playlists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "service_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnail_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uploader", + "columnName": "uploader", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "streamCount", + "columnName": "stream_count", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_remote_playlists_name", + "unique": false, + "columnNames": [ + "name" + ], + "createSql": "CREATE INDEX `index_remote_playlists_name` ON `${TABLE_NAME}` (`name`)" + }, + { + "name": "index_remote_playlists_service_id_url", + "unique": true, + "columnNames": [ + "service_id", + "url" + ], + "createSql": "CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "feed", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `subscription_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "streamId", + "columnName": "stream_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subscriptionId", + "columnName": "subscription_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "stream_id", + "subscription_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_feed_subscription_id", + "unique": false, + "columnNames": [ + "subscription_id" + ], + "createSql": "CREATE INDEX `index_feed_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)" + } + ], + "foreignKeys": [ + { + "table": "streams", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "stream_id" + ], + "referencedColumns": [ + "uid" + ] + }, + { + "table": "subscriptions", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "subscription_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "feed_group", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon_id` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "iconId", + "columnName": "icon_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "feed_group_subscription_join", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`group_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`group_id`, `subscription_id`), FOREIGN KEY(`group_id`) REFERENCES `feed_group`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "feedGroupId", + "columnName": "group_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subscriptionId", + "columnName": "subscription_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "group_id", + "subscription_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_feed_group_subscription_join_subscription_id", + "unique": false, + "columnNames": [ + "subscription_id" + ], + "createSql": "CREATE INDEX `index_feed_group_subscription_join_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)" + } + ], + "foreignKeys": [ + { + "table": "feed_group", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "group_id" + ], + "referencedColumns": [ + "uid" + ] + }, + { + "table": "subscriptions", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "subscription_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + } + ], + "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, \"ecffbb2ea251aeb38a8f508acf2aa404\")" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java index 13c643edb..81b5dd72f 100644 --- a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java @@ -9,7 +9,8 @@ import androidx.room.Room; import org.schabi.newpipe.database.AppDatabase; import static org.schabi.newpipe.database.AppDatabase.DATABASE_NAME; -import static org.schabi.newpipe.database.Migrations.MIGRATION_11_12; +import static org.schabi.newpipe.database.Migrations.MIGRATION_1_2; +import static org.schabi.newpipe.database.Migrations.MIGRATION_2_3; public final class NewPipeDatabase { @@ -22,7 +23,7 @@ public final class NewPipeDatabase { private static AppDatabase getDatabase(Context context) { return Room .databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME) - .addMigrations(MIGRATION_11_12) + .addMigrations(MIGRATION_1_2, MIGRATION_2_3) .build(); } diff --git a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java index d374f254b..49acd41a0 100644 --- a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java @@ -4,6 +4,11 @@ import androidx.room.Database; import androidx.room.RoomDatabase; import androidx.room.TypeConverters; +import org.schabi.newpipe.database.feed.dao.FeedDAO; +import org.schabi.newpipe.database.feed.dao.FeedGroupDAO; +import org.schabi.newpipe.database.feed.model.FeedEntity; +import org.schabi.newpipe.database.feed.model.FeedGroupEntity; +import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity; import org.schabi.newpipe.database.history.dao.SearchHistoryDAO; import org.schabi.newpipe.database.history.dao.StreamHistoryDAO; import org.schabi.newpipe.database.history.model.SearchHistoryEntry; @@ -21,35 +26,32 @@ import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.database.subscription.SubscriptionDAO; import org.schabi.newpipe.database.subscription.SubscriptionEntity; -import static org.schabi.newpipe.database.Migrations.DB_VER_12_0; +import static org.schabi.newpipe.database.Migrations.DB_VER_3; @TypeConverters({Converters.class}) @Database( entities = { SubscriptionEntity.class, SearchHistoryEntry.class, StreamEntity.class, StreamHistoryEntity.class, StreamStateEntity.class, - PlaylistEntity.class, PlaylistStreamEntity.class, PlaylistRemoteEntity.class + PlaylistEntity.class, PlaylistStreamEntity.class, PlaylistRemoteEntity.class, + FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class }, - version = DB_VER_12_0, - exportSchema = false + version = DB_VER_3 ) public abstract class AppDatabase extends RoomDatabase { - public static final String DATABASE_NAME = "newpipe.db"; - public abstract SubscriptionDAO subscriptionDAO(); - public abstract SearchHistoryDAO searchHistoryDAO(); public abstract StreamDAO streamDAO(); - public abstract StreamHistoryDAO streamHistoryDAO(); - public abstract StreamStateDAO streamStateDAO(); public abstract PlaylistDAO playlistDAO(); - public abstract PlaylistStreamDAO playlistStreamDAO(); - public abstract PlaylistRemoteDAO playlistRemoteDAO(); + + public abstract FeedDAO feedDAO(); + public abstract FeedGroupDAO feedGroupDAO(); + public abstract SubscriptionDAO subscriptionDAO(); } diff --git a/app/src/main/java/org/schabi/newpipe/database/Migrations.java b/app/src/main/java/org/schabi/newpipe/database/Migrations.java index 07d9749b2..4724bf21d 100644 --- a/app/src/main/java/org/schabi/newpipe/database/Migrations.java +++ b/app/src/main/java/org/schabi/newpipe/database/Migrations.java @@ -8,14 +8,14 @@ import android.util.Log; import org.schabi.newpipe.BuildConfig; public class Migrations { - - public static final int DB_VER_11_0 = 1; - public static final int DB_VER_12_0 = 2; + public static final int DB_VER_1 = 1; + public static final int DB_VER_2 = 2; + public static final int DB_VER_3 = 3; public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release"); private static final String TAG = Migrations.class.getName(); - public static final Migration MIGRATION_11_12 = new Migration(DB_VER_11_0, DB_VER_12_0) { + public static final Migration MIGRATION_1_2 = new Migration(DB_VER_1, DB_VER_2) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { if(DEBUG) { @@ -71,4 +71,29 @@ public class Migrations { } } }; + + public static final Migration MIGRATION_2_3 = new Migration(DB_VER_2, DB_VER_3) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + // Add NOT NULLs and new fields + database.execSQL("CREATE TABLE IF NOT EXISTS streams_new " + + "(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, service_id INTEGER NOT NULL, url TEXT NOT NULL, title TEXT NOT NULL, stream_type TEXT NOT NULL," + + " duration INTEGER NOT NULL, uploader TEXT NOT NULL, thumbnail_url TEXT, view_count INTEGER, textual_upload_date TEXT, upload_date INTEGER)"); + + database.execSQL("INSERT INTO streams_new (uid, service_id, url, title, stream_type, duration, uploader, thumbnail_url, view_count, textual_upload_date, upload_date)"+ + " SELECT uid, service_id, url, title, stream_type, duration, uploader, thumbnail_url, NULL, NULL, NULL FROM streams"); + + database.execSQL("DROP TABLE streams"); + database.execSQL("ALTER TABLE streams_new RENAME TO streams"); + database.execSQL("CREATE UNIQUE INDEX index_streams_service_id_url ON streams (service_id, url)"); + + // Tables for feed feature + database.execSQL("CREATE TABLE IF NOT EXISTS feed (stream_id INTEGER NOT NULL, subscription_id INTEGER NOT NULL, PRIMARY KEY(stream_id, subscription_id), FOREIGN KEY(stream_id) REFERENCES streams(uid) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(subscription_id) REFERENCES subscriptions(uid) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)"); + database.execSQL("CREATE INDEX index_feed_subscription_id ON feed (subscription_id)"); + database.execSQL("CREATE TABLE IF NOT EXISTS feed_group (uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name TEXT NOT NULL, icon_id INTEGER NOT NULL)"); + database.execSQL("CREATE TABLE IF NOT EXISTS feed_group_subscription_join (group_id INTEGER NOT NULL, subscription_id INTEGER NOT NULL, PRIMARY KEY(group_id, subscription_id), FOREIGN KEY(group_id) REFERENCES feed_group(uid) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(subscription_id) REFERENCES subscriptions(uid) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)"); + database.execSQL("CREATE INDEX index_feed_group_subscription_join_subscription_id ON feed_group_subscription_join (subscription_id)"); + } + }; + } diff --git a/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt b/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt new file mode 100644 index 000000000..668f98d0a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt @@ -0,0 +1,47 @@ +package org.schabi.newpipe.database.feed.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import io.reactivex.Flowable +import org.schabi.newpipe.database.feed.model.FeedEntity +import org.schabi.newpipe.database.stream.model.StreamEntity + +@Dao +abstract class FeedDAO { + @Query("DELETE FROM feed") + abstract fun deleteAll(): Int + + @Query(""" + SELECT s.* FROM streams s + + INNER JOIN feed f + ON s.uid = f.stream_id + + ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC + """) + abstract fun getAllStreams(): Flowable> + + @Query(""" + SELECT s.* FROM streams s + + INNER JOIN feed f + ON s.uid = f.stream_id + + INNER JOIN feed_group_subscription_join fgs + ON fgs.subscription_id = f.subscription_id + + INNER JOIN feed_group fg + ON fg.uid = fgs.group_id + + WHERE fgs.group_id = :groupId + """) + abstract fun getAllStreamsFromGroup(groupId: Long): Flowable> + + @Insert(onConflict = OnConflictStrategy.FAIL) + abstract fun insert(feedEntity: FeedEntity) + + @Insert(onConflict = OnConflictStrategy.FAIL) + abstract fun insertAll(entities: List): List +} diff --git a/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedGroupDAO.kt b/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedGroupDAO.kt new file mode 100644 index 000000000..233c5e064 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedGroupDAO.kt @@ -0,0 +1,17 @@ +package org.schabi.newpipe.database.feed.dao + +import androidx.room.* +import io.reactivex.Flowable +import org.schabi.newpipe.database.feed.model.FeedGroupEntity + +@Dao +abstract class FeedGroupDAO { + @Query("DELETE FROM feed_group") + abstract fun deleteAll(): Int + + @Query("SELECT * FROM feed_group") + abstract fun getAll(): Flowable> + + @Insert(onConflict = OnConflictStrategy.ABORT) + abstract fun insert(feedEntity: FeedGroupEntity) +} diff --git a/app/src/main/java/org/schabi/newpipe/database/feed/model/FeedEntity.kt b/app/src/main/java/org/schabi/newpipe/database/feed/model/FeedEntity.kt new file mode 100644 index 000000000..e73af7fcf --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/feed/model/FeedEntity.kt @@ -0,0 +1,43 @@ +package org.schabi.newpipe.database.feed.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import org.schabi.newpipe.database.feed.model.FeedEntity.Companion.FEED_TABLE +import org.schabi.newpipe.database.feed.model.FeedEntity.Companion.STREAM_ID +import org.schabi.newpipe.database.feed.model.FeedEntity.Companion.SUBSCRIPTION_ID +import org.schabi.newpipe.database.stream.model.StreamEntity +import org.schabi.newpipe.database.subscription.SubscriptionEntity + +@Entity(tableName = FEED_TABLE, + primaryKeys = [STREAM_ID, SUBSCRIPTION_ID], + indices = [Index(SUBSCRIPTION_ID)], + foreignKeys = [ + ForeignKey( + entity = StreamEntity::class, + parentColumns = [StreamEntity.STREAM_ID], + childColumns = [STREAM_ID], + onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true), + ForeignKey( + entity = SubscriptionEntity::class, + parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID], + childColumns = [SUBSCRIPTION_ID], + onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true) + ] +) +data class FeedEntity( + @ColumnInfo(name = STREAM_ID) + var streamId: Long, + + @ColumnInfo(name = SUBSCRIPTION_ID) + var subscriptionId: Long +) { + + companion object { + const val FEED_TABLE = "feed" + + const val STREAM_ID = "stream_id" + const val SUBSCRIPTION_ID = "subscription_id" + } +} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/database/feed/model/FeedGroupEntity.kt b/app/src/main/java/org/schabi/newpipe/database/feed/model/FeedGroupEntity.kt new file mode 100644 index 000000000..cd919ec05 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/feed/model/FeedGroupEntity.kt @@ -0,0 +1,27 @@ +package org.schabi.newpipe.database.feed.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import org.schabi.newpipe.database.feed.model.FeedGroupEntity.Companion.FEED_GROUP_TABLE + +@Entity(tableName = FEED_GROUP_TABLE) +data class FeedGroupEntity( + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = ID) + val uid: Long, + + @ColumnInfo(name = NAME) + var name: String, + + @ColumnInfo(name = ICON) + var iconId: Int +) { + companion object { + const val FEED_GROUP_TABLE = "feed_group" + + const val ID = "uid" + const val NAME = "name" + const val ICON = "icon_id" + } +} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/database/feed/model/FeedGroupSubscriptionEntity.kt b/app/src/main/java/org/schabi/newpipe/database/feed/model/FeedGroupSubscriptionEntity.kt new file mode 100644 index 000000000..55fe5d4df --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/feed/model/FeedGroupSubscriptionEntity.kt @@ -0,0 +1,45 @@ +package org.schabi.newpipe.database.feed.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.ForeignKey.CASCADE +import androidx.room.Index +import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity.Companion.FEED_GROUP_SUBSCRIPTION_TABLE +import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity.Companion.GROUP_ID +import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity.Companion.SUBSCRIPTION_ID +import org.schabi.newpipe.database.subscription.SubscriptionEntity + +@Entity( + tableName = FEED_GROUP_SUBSCRIPTION_TABLE, + primaryKeys = [GROUP_ID, SUBSCRIPTION_ID], + indices = [Index(SUBSCRIPTION_ID)], + foreignKeys = [ + ForeignKey( + entity = FeedGroupEntity::class, + parentColumns = [FeedGroupEntity.ID], + childColumns = [GROUP_ID], + onDelete = CASCADE, onUpdate = CASCADE, deferred = true), + + ForeignKey( + entity = SubscriptionEntity::class, + parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID], + childColumns = [SUBSCRIPTION_ID], + onDelete = CASCADE, onUpdate = CASCADE, deferred = true) + ] +) +data class FeedGroupSubscriptionEntity( + @ColumnInfo(name = GROUP_ID) + var feedGroupId: Long, + + @ColumnInfo(name = SUBSCRIPTION_ID) + var subscriptionId: Long +) { + + companion object { + const val FEED_GROUP_SUBSCRIPTION_TABLE = "feed_group_subscription_join" + + const val GROUP_ID = "group_id" + const val SUBSCRIPTION_ID = "subscription_id" + } +} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntry.java b/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntry.java deleted file mode 100644 index ad66451e4..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntry.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.schabi.newpipe.database.history.model; - -import androidx.room.ColumnInfo; - -import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.extractor.stream.StreamType; - -import java.util.Date; - -public class StreamHistoryEntry { - @ColumnInfo(name = StreamEntity.STREAM_ID) - final public long uid; - @ColumnInfo(name = StreamEntity.STREAM_SERVICE_ID) - final public int serviceId; - @ColumnInfo(name = StreamEntity.STREAM_URL) - final public String url; - @ColumnInfo(name = StreamEntity.STREAM_TITLE) - final public String title; - @ColumnInfo(name = StreamEntity.STREAM_TYPE) - final public StreamType streamType; - @ColumnInfo(name = StreamEntity.STREAM_DURATION) - final public long duration; - @ColumnInfo(name = StreamEntity.STREAM_UPLOADER) - final public String uploader; - @ColumnInfo(name = StreamEntity.STREAM_THUMBNAIL_URL) - final public String thumbnailUrl; - @ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID) - final public long streamId; - @ColumnInfo(name = StreamHistoryEntity.STREAM_ACCESS_DATE) - final public Date accessDate; - @ColumnInfo(name = StreamHistoryEntity.STREAM_REPEAT_COUNT) - final public long repeatCount; - - public StreamHistoryEntry(long uid, int serviceId, String url, String title, - StreamType streamType, long duration, String uploader, - String thumbnailUrl, long streamId, Date accessDate, - long repeatCount) { - this.uid = uid; - this.serviceId = serviceId; - this.url = url; - this.title = title; - this.streamType = streamType; - this.duration = duration; - this.uploader = uploader; - this.thumbnailUrl = thumbnailUrl; - this.streamId = streamId; - this.accessDate = accessDate; - this.repeatCount = repeatCount; - } - - public StreamHistoryEntity toStreamHistoryEntity() { - return new StreamHistoryEntity(streamId, accessDate, repeatCount); - } - - public boolean hasEqualValues(StreamHistoryEntry other) { - return this.uid == other.uid && streamId == other.streamId && - accessDate.compareTo(other.accessDate) == 0; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntry.kt b/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntry.kt new file mode 100644 index 000000000..e06ecee36 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntry.kt @@ -0,0 +1,30 @@ +package org.schabi.newpipe.database.history.model + +import androidx.room.ColumnInfo +import androidx.room.Embedded +import org.schabi.newpipe.database.stream.model.StreamEntity +import java.util.* + +data class StreamHistoryEntry( + @Embedded + val streamEntity: StreamEntity, + + @ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID) + val streamId: Long, + + @ColumnInfo(name = StreamHistoryEntity.STREAM_ACCESS_DATE) + val accessDate: Date, + + @ColumnInfo(name = StreamHistoryEntity.STREAM_REPEAT_COUNT) + val repeatCount: Long +) { + + fun toStreamHistoryEntity(): StreamHistoryEntity { + return StreamHistoryEntity(streamId, accessDate, repeatCount) + } + + fun hasEqualValues(other: StreamHistoryEntry): Boolean { + return this.streamEntity.uid == other.streamEntity.uid && streamId == other.streamId && + accessDate.compareTo(other.accessDate) == 0 + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.java deleted file mode 100644 index fb45c3564..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.schabi.newpipe.database.playlist; - -import androidx.room.ColumnInfo; - -import org.schabi.newpipe.database.LocalItem; -import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; -import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.extractor.stream.StreamType; - -public class PlaylistStreamEntry implements LocalItem { - @ColumnInfo(name = StreamEntity.STREAM_ID) - final public long uid; - @ColumnInfo(name = StreamEntity.STREAM_SERVICE_ID) - final public int serviceId; - @ColumnInfo(name = StreamEntity.STREAM_URL) - final public String url; - @ColumnInfo(name = StreamEntity.STREAM_TITLE) - final public String title; - @ColumnInfo(name = StreamEntity.STREAM_TYPE) - final public StreamType streamType; - @ColumnInfo(name = StreamEntity.STREAM_DURATION) - final public long duration; - @ColumnInfo(name = StreamEntity.STREAM_UPLOADER) - final public String uploader; - @ColumnInfo(name = StreamEntity.STREAM_THUMBNAIL_URL) - final public String thumbnailUrl; - @ColumnInfo(name = PlaylistStreamEntity.JOIN_STREAM_ID) - final public long streamId; - @ColumnInfo(name = PlaylistStreamEntity.JOIN_INDEX) - final public int joinIndex; - - public PlaylistStreamEntry(long uid, int serviceId, String url, String title, - StreamType streamType, long duration, String uploader, - String thumbnailUrl, long streamId, int joinIndex) { - this.uid = uid; - this.serviceId = serviceId; - this.url = url; - this.title = title; - this.streamType = streamType; - this.duration = duration; - this.uploader = uploader; - this.thumbnailUrl = thumbnailUrl; - this.streamId = streamId; - this.joinIndex = joinIndex; - } - - public StreamInfoItem toStreamInfoItem() throws IllegalArgumentException { - StreamInfoItem item = new StreamInfoItem(serviceId, url, title, streamType); - item.setThumbnailUrl(thumbnailUrl); - item.setUploaderName(uploader); - item.setDuration(duration); - return item; - } - - @Override - public LocalItemType getLocalItemType() { - return LocalItemType.PLAYLIST_STREAM_ITEM; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.kt new file mode 100644 index 000000000..afaf599b9 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.kt @@ -0,0 +1,34 @@ +package org.schabi.newpipe.database.playlist + +import androidx.room.ColumnInfo +import androidx.room.Embedded +import org.schabi.newpipe.database.LocalItem +import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity +import org.schabi.newpipe.database.stream.model.StreamEntity +import org.schabi.newpipe.extractor.stream.StreamInfoItem + +class PlaylistStreamEntry( + @Embedded + val streamEntity: StreamEntity, + + @ColumnInfo(name = PlaylistStreamEntity.JOIN_STREAM_ID) + val streamId: Long, + + @ColumnInfo(name = PlaylistStreamEntity.JOIN_INDEX) + val joinIndex: Int +) : LocalItem { + + @Throws(IllegalArgumentException::class) + fun toStreamInfoItem(): StreamInfoItem { + val item = StreamInfoItem(streamEntity.serviceId, streamEntity.url, streamEntity.title, streamEntity.streamType) + item.duration = streamEntity.duration + item.uploaderName = streamEntity.uploader + item.thumbnailUrl = streamEntity.thumbnailUrl + + return item + } + + override fun getLocalItemType(): LocalItem.LocalItemType { + return LocalItem.LocalItemType.PLAYLIST_STREAM_ITEM + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java b/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java deleted file mode 100644 index 9b61eb469..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java +++ /dev/null @@ -1,69 +0,0 @@ -package org.schabi.newpipe.database.stream; - -import androidx.room.ColumnInfo; - -import org.schabi.newpipe.database.LocalItem; -import org.schabi.newpipe.database.history.model.StreamHistoryEntity; -import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.extractor.stream.StreamType; - -import java.util.Date; - -public class StreamStatisticsEntry implements LocalItem { - final public static String STREAM_LATEST_DATE = "latestAccess"; - final public static String STREAM_WATCH_COUNT = "watchCount"; - - @ColumnInfo(name = StreamEntity.STREAM_ID) - final public long uid; - @ColumnInfo(name = StreamEntity.STREAM_SERVICE_ID) - final public int serviceId; - @ColumnInfo(name = StreamEntity.STREAM_URL) - final public String url; - @ColumnInfo(name = StreamEntity.STREAM_TITLE) - final public String title; - @ColumnInfo(name = StreamEntity.STREAM_TYPE) - final public StreamType streamType; - @ColumnInfo(name = StreamEntity.STREAM_DURATION) - final public long duration; - @ColumnInfo(name = StreamEntity.STREAM_UPLOADER) - final public String uploader; - @ColumnInfo(name = StreamEntity.STREAM_THUMBNAIL_URL) - final public String thumbnailUrl; - @ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID) - final public long streamId; - @ColumnInfo(name = StreamStatisticsEntry.STREAM_LATEST_DATE) - final public Date latestAccessDate; - @ColumnInfo(name = StreamStatisticsEntry.STREAM_WATCH_COUNT) - final public long watchCount; - - public StreamStatisticsEntry(long uid, int serviceId, String url, String title, - StreamType streamType, long duration, String uploader, - String thumbnailUrl, long streamId, Date latestAccessDate, - long watchCount) { - this.uid = uid; - this.serviceId = serviceId; - this.url = url; - this.title = title; - this.streamType = streamType; - this.duration = duration; - this.uploader = uploader; - this.thumbnailUrl = thumbnailUrl; - this.streamId = streamId; - this.latestAccessDate = latestAccessDate; - this.watchCount = watchCount; - } - - public StreamInfoItem toStreamInfoItem() { - StreamInfoItem item = new StreamInfoItem(serviceId, url, title, streamType); - item.setDuration(duration); - item.setUploaderName(uploader); - item.setThumbnailUrl(thumbnailUrl); - return item; - } - - @Override - public LocalItemType getLocalItemType() { - return LocalItemType.STATISTIC_STREAM_ITEM; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.kt b/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.kt new file mode 100644 index 000000000..70081f8ed --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.kt @@ -0,0 +1,42 @@ +package org.schabi.newpipe.database.stream + +import androidx.room.ColumnInfo +import androidx.room.Embedded +import org.schabi.newpipe.database.LocalItem +import org.schabi.newpipe.database.history.model.StreamHistoryEntity +import org.schabi.newpipe.database.stream.model.StreamEntity +import org.schabi.newpipe.extractor.stream.StreamInfoItem +import java.util.* + +class StreamStatisticsEntry( + @Embedded + val streamEntity: StreamEntity, + + @ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID) + val streamId: Long, + + @ColumnInfo(name = STREAM_LATEST_DATE) + val latestAccessDate: Date, + + @ColumnInfo(name = STREAM_WATCH_COUNT) + val watchCount: Long +) : LocalItem { + + fun toStreamInfoItem(): StreamInfoItem { + val item = StreamInfoItem(streamEntity.serviceId, streamEntity.url, streamEntity.title, streamEntity.streamType) + item.duration = streamEntity.duration + item.uploaderName = streamEntity.uploader + item.thumbnailUrl = streamEntity.thumbnailUrl + + return item + } + + override fun getLocalItemType(): LocalItem.LocalItemType { + return LocalItem.LocalItemType.STATISTIC_STREAM_ITEM + } + + companion object { + const val STREAM_LATEST_DATE = "latestAccess" + const val STREAM_WATCH_COUNT = "watchCount" + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java deleted file mode 100644 index c89f6163f..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java +++ /dev/null @@ -1,98 +0,0 @@ -package org.schabi.newpipe.database.stream.dao; - -import androidx.room.Dao; -import androidx.room.Insert; -import androidx.room.OnConflictStrategy; -import androidx.room.Query; -import androidx.room.Transaction; - -import org.schabi.newpipe.database.BasicDAO; -import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; -import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.database.history.model.StreamHistoryEntity; - -import java.util.ArrayList; -import java.util.List; - -import io.reactivex.Flowable; - -import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE; -import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID; -import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_SERVICE_ID; -import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; -import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_URL; -import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; - -@Dao -public abstract class StreamDAO implements BasicDAO { - @Override - @Query("SELECT * FROM " + STREAM_TABLE) - public abstract Flowable> getAll(); - - @Override - @Query("DELETE FROM " + STREAM_TABLE) - public abstract int deleteAll(); - - @Override - @Query("SELECT * FROM " + STREAM_TABLE + " WHERE " + STREAM_SERVICE_ID + " = :serviceId") - public abstract Flowable> listByService(int serviceId); - - @Query("SELECT * FROM " + STREAM_TABLE + " WHERE " + - STREAM_URL + " = :url AND " + - STREAM_SERVICE_ID + " = :serviceId") - public abstract Flowable> getStream(long serviceId, String url); - - @Insert(onConflict = OnConflictStrategy.IGNORE) - abstract void silentInsertAllInternal(final List streams); - - @Query("SELECT " + STREAM_ID + " FROM " + STREAM_TABLE + " WHERE " + - STREAM_URL + " = :url AND " + - STREAM_SERVICE_ID + " = :serviceId") - abstract Long getStreamIdInternal(long serviceId, String url); - - @Transaction - public long upsert(StreamEntity stream) { - final Long streamIdCandidate = getStreamIdInternal(stream.getServiceId(), stream.getUrl()); - - if (streamIdCandidate == null) { - return insert(stream); - } else { - stream.setUid(streamIdCandidate); - update(stream); - return streamIdCandidate; - } - } - - @Transaction - public List upsertAll(List streams) { - silentInsertAllInternal(streams); - - final List streamIds = new ArrayList<>(streams.size()); - for (StreamEntity stream : streams) { - final Long streamId = getStreamIdInternal(stream.getServiceId(), stream.getUrl()); - if (streamId == null) { - throw new IllegalStateException("StreamID cannot be null just after insertion."); - } - - streamIds.add(streamId); - stream.setUid(streamId); - } - - update(streams); - return streamIds; - } - - @Query("DELETE FROM " + STREAM_TABLE + " WHERE " + STREAM_ID + - " NOT IN " + - "(SELECT DISTINCT " + STREAM_ID + " FROM " + STREAM_TABLE + - - " LEFT JOIN " + STREAM_HISTORY_TABLE + - " ON " + STREAM_ID + " = " + - StreamHistoryEntity.STREAM_HISTORY_TABLE + "." + StreamHistoryEntity.JOIN_STREAM_ID + - - " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE + - " ON " + STREAM_ID + " = " + - PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE + "." + PlaylistStreamEntity.JOIN_STREAM_ID + - ")") - public abstract int deleteOrphans(); -} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.kt b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.kt new file mode 100644 index 000000000..ed99d7e81 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.kt @@ -0,0 +1,116 @@ +package org.schabi.newpipe.database.stream.dao + +import androidx.room.* +import io.reactivex.Flowable +import org.schabi.newpipe.database.BasicDAO +import org.schabi.newpipe.database.stream.model.StreamEntity +import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID +import org.schabi.newpipe.extractor.stream.StreamType +import org.schabi.newpipe.extractor.stream.StreamType.* +import java.util.* +import kotlin.collections.ArrayList + +@Dao +abstract class StreamDAO : BasicDAO { + @Query("SELECT * FROM streams") + abstract override fun getAll(): Flowable> + + @Query("DELETE FROM streams") + abstract override fun deleteAll(): Int + + @Query("SELECT * FROM streams WHERE service_id = :serviceId") + abstract override fun listByService(serviceId: Int): Flowable> + + @Query("SELECT * FROM streams WHERE url = :url AND service_id = :serviceId") + abstract fun getStream(serviceId: Long, url: String): Flowable> + + @Insert(onConflict = OnConflictStrategy.IGNORE) + internal abstract fun silentInsertInternal(stream: StreamEntity): Long + + @Insert(onConflict = OnConflictStrategy.IGNORE) + internal abstract fun silentInsertAllInternal(streams: List): List + + @Query(""" + SELECT uid, stream_type, textual_upload_date, upload_date FROM streams + WHERE url = :url AND service_id = :serviceId + """) + internal abstract fun getMinimalStreamForCompare(serviceId: Int, url: String): StreamCompareFeed? + + @Transaction + open fun upsert(newerStream: StreamEntity): Long { + val uid = silentInsertInternal(newerStream) + + if (uid != -1L) { + newerStream.uid = uid + return uid + } + + compareAndUpdateStream(newerStream) + + update(newerStream) + return newerStream.uid + } + + @Transaction + open fun upsertAll(streams: List): List { + val insertUidList = silentInsertAllInternal(streams) + + val streamIds = ArrayList(streams.size) + for ((index, uid) in insertUidList.withIndex()) { + val newerStream = streams[index] + if (uid != -1L) { + streamIds.add(uid) + newerStream.uid = uid + continue + } + + compareAndUpdateStream(newerStream) + streamIds.add(newerStream.uid) + } + + update(streams) + return streamIds + } + + private fun compareAndUpdateStream(newerStream: StreamEntity) { + val existentMinimalStream = getMinimalStreamForCompare(newerStream.serviceId, newerStream.url) + ?: throw IllegalStateException("Stream cannot be null just after insertion.") + newerStream.uid = existentMinimalStream.uid + + val isNewerStreamLive = newerStream.streamType == AUDIO_LIVE_STREAM || newerStream.streamType == LIVE_STREAM + if (!isNewerStreamLive) { + if (existentMinimalStream.uploadDate != null) newerStream.uploadDate = existentMinimalStream.uploadDate + if (existentMinimalStream.textualUploadDate != null) newerStream.textualUploadDate = existentMinimalStream.textualUploadDate + } + } + + @Query(""" + DELETE FROM streams WHERE + + NOT EXISTS (SELECT 1 FROM stream_history sh + WHERE sh.stream_id = streams.uid) + + AND NOT EXISTS (SELECT 1 FROM playlist_stream_join ps + WHERE ps.stream_id = streams.uid) + + AND NOT EXISTS (SELECT 1 FROM feed f + WHERE f.stream_id = streams.uid) + """) + abstract fun deleteOrphans(): Int + + /** + * Minimal entry class used when comparing/updating an existent stream. + */ + internal data class StreamCompareFeed( + @ColumnInfo(name = STREAM_ID) + var uid: Long = 0, + + @field:ColumnInfo(name = StreamEntity.STREAM_TYPE) + var streamType: StreamType, + + @field:ColumnInfo(name = StreamEntity.STREAM_TEXTUAL_UPLOAD_DATE) + var textualUploadDate: String? = null, + + @field:ColumnInfo(name = StreamEntity.STREAM_UPLOAD_DATE) + var uploadDate: Date? = null) +} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java deleted file mode 100644 index 1f26e214d..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java +++ /dev/null @@ -1,153 +0,0 @@ -package org.schabi.newpipe.database.stream.model; - -import androidx.room.ColumnInfo; -import androidx.room.Entity; -import androidx.room.Ignore; -import androidx.room.Index; -import androidx.room.PrimaryKey; - -import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.extractor.stream.StreamType; -import org.schabi.newpipe.player.playqueue.PlayQueueItem; -import org.schabi.newpipe.util.Constants; - -import java.io.Serializable; - -import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_SERVICE_ID; -import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; -import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_URL; - -@Entity(tableName = STREAM_TABLE, - indices = {@Index(value = {STREAM_SERVICE_ID, STREAM_URL}, unique = true)}) -public class StreamEntity implements Serializable { - - final public static String STREAM_TABLE = "streams"; - final public static String STREAM_ID = "uid"; - final public static String STREAM_SERVICE_ID = "service_id"; - final public static String STREAM_URL = "url"; - final public static String STREAM_TITLE = "title"; - final public static String STREAM_TYPE = "stream_type"; - final public static String STREAM_DURATION = "duration"; - final public static String STREAM_UPLOADER = "uploader"; - final public static String STREAM_THUMBNAIL_URL = "thumbnail_url"; - - @PrimaryKey(autoGenerate = true) - @ColumnInfo(name = STREAM_ID) - private long uid = 0; - - @ColumnInfo(name = STREAM_SERVICE_ID) - private int serviceId = Constants.NO_SERVICE_ID; - - @ColumnInfo(name = STREAM_URL) - private String url; - - @ColumnInfo(name = STREAM_TITLE) - private String title; - - @ColumnInfo(name = STREAM_TYPE) - private StreamType streamType; - - @ColumnInfo(name = STREAM_DURATION) - private Long duration; - - @ColumnInfo(name = STREAM_UPLOADER) - private String uploader; - - @ColumnInfo(name = STREAM_THUMBNAIL_URL) - private String thumbnailUrl; - - public StreamEntity(final int serviceId, final String title, final String url, - final StreamType streamType, final String thumbnailUrl, final String uploader, - final long duration) { - this.serviceId = serviceId; - this.title = title; - this.url = url; - this.streamType = streamType; - this.thumbnailUrl = thumbnailUrl; - this.uploader = uploader; - this.duration = duration; - } - - @Ignore - public StreamEntity(final StreamInfoItem item) { - this(item.getServiceId(), item.getName(), item.getUrl(), item.getStreamType(), item.getThumbnailUrl(), - item.getUploaderName(), item.getDuration()); - } - - @Ignore - public StreamEntity(final StreamInfo info) { - this(info.getServiceId(), info.getName(), info.getUrl(), info.getStreamType(), info.getThumbnailUrl(), - info.getUploaderName(), info.getDuration()); - } - - @Ignore - public StreamEntity(final PlayQueueItem item) { - this(item.getServiceId(), item.getTitle(), item.getUrl(), item.getStreamType(), - item.getThumbnailUrl(), item.getUploader(), item.getDuration()); - } - - public long getUid() { - return uid; - } - - public void setUid(long uid) { - this.uid = uid; - } - - public int getServiceId() { - return serviceId; - } - - public void setServiceId(int serviceId) { - this.serviceId = serviceId; - } - - public String getUrl() { - return url; - } - - public void setUrl(String url) { - this.url = url; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public StreamType getStreamType() { - return streamType; - } - - public void setStreamType(StreamType type) { - this.streamType = type; - } - - public Long getDuration() { - return duration; - } - - public void setDuration(Long duration) { - this.duration = duration; - } - - public String getUploader() { - return uploader; - } - - public void setUploader(String uploader) { - this.uploader = uploader; - } - - public String getThumbnailUrl() { - return thumbnailUrl; - } - - public void setThumbnailUrl(String thumbnailUrl) { - this.thumbnailUrl = thumbnailUrl; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.kt b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.kt new file mode 100644 index 000000000..5e9464df9 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.kt @@ -0,0 +1,107 @@ +package org.schabi.newpipe.database.stream.model + +import androidx.room.* +import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_SERVICE_ID +import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_TABLE +import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_URL +import org.schabi.newpipe.extractor.localization.DateWrapper +import org.schabi.newpipe.extractor.stream.StreamInfo +import org.schabi.newpipe.extractor.stream.StreamInfoItem +import org.schabi.newpipe.extractor.stream.StreamType +import org.schabi.newpipe.player.playqueue.PlayQueueItem +import java.io.Serializable +import java.util.* + +@Entity(tableName = STREAM_TABLE, + indices = [ + Index(value = [STREAM_SERVICE_ID, STREAM_URL], unique = true) + ] +) +data class StreamEntity( + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = STREAM_ID) + var uid: Long = 0, + + @ColumnInfo(name = STREAM_SERVICE_ID) + var serviceId: Int, + + @ColumnInfo(name = STREAM_URL) + var url: String, + + @ColumnInfo(name = STREAM_TITLE) + var title: String, + + @ColumnInfo(name = STREAM_TYPE) + var streamType: StreamType, + + @ColumnInfo(name = STREAM_DURATION) + var duration: Long, + + @ColumnInfo(name = STREAM_UPLOADER) + var uploader: String, + + @ColumnInfo(name = STREAM_THUMBNAIL_URL) + var thumbnailUrl: String? = null, + + @ColumnInfo(name = STREAM_VIEWS) + var viewCount: Long? = null, + + @ColumnInfo(name = STREAM_TEXTUAL_UPLOAD_DATE) + var textualUploadDate: String? = null, + + @ColumnInfo(name = STREAM_UPLOAD_DATE) + var uploadDate: Date? = null +) : Serializable { + + @Ignore + constructor(item: StreamInfoItem) : this( + serviceId = item.serviceId, url = item.url, title = item.name, + streamType = item.streamType, duration = item.duration, uploader = item.uploaderName, + thumbnailUrl = item.thumbnailUrl, viewCount = item.viewCount, + textualUploadDate = item.textualUploadDate, uploadDate = item.uploadDate?.date()?.time + ) + + @Ignore + constructor(info: StreamInfo) : this( + serviceId = info.serviceId, url = info.url, title = info.name, + streamType = info.streamType, duration = info.duration, uploader = info.uploaderName, + thumbnailUrl = info.thumbnailUrl, viewCount = info.viewCount, + textualUploadDate = info.textualUploadDate, uploadDate = info.uploadDate?.date()?.time + ) + + @Ignore + constructor(item: PlayQueueItem) : this( + serviceId = item.serviceId, url = item.url, title = item.title, + streamType = item.streamType, duration = item.duration, uploader = item.uploader, + thumbnailUrl = item.thumbnailUrl + ) + + fun toStreamInfoItem(): StreamInfoItem { + val item = StreamInfoItem(serviceId, url, title, streamType) + item.duration = duration + item.uploaderName = uploader + item.thumbnailUrl = thumbnailUrl + + if (viewCount != null) item.viewCount = viewCount as Long + item.textualUploadDate = textualUploadDate + item.uploadDate = uploadDate?.let { DateWrapper(Calendar.getInstance().apply { time = it }) } + + return item + } + + companion object { + const val STREAM_TABLE = "streams" + const val STREAM_ID = "uid" + const val STREAM_SERVICE_ID = "service_id" + const val STREAM_URL = "url" + const val STREAM_TITLE = "title" + const val STREAM_TYPE = "stream_type" + const val STREAM_DURATION = "duration" + const val STREAM_UPLOADER = "uploader" + const val STREAM_THUMBNAIL_URL = "thumbnail_url" + + const val STREAM_VIEWS = "view_count" + const val STREAM_TEXTUAL_UPLOAD_DATE = "textual_upload_date" + const val STREAM_UPLOAD_DATE = "upload_date" + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.java b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.java index 0869d60ff..03df797a4 100644 --- a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.java @@ -23,6 +23,9 @@ public abstract class SubscriptionDAO implements BasicDAO { @Query("SELECT * FROM " + SUBSCRIPTION_TABLE) public abstract Flowable> getAll(); + @Query("SELECT COUNT(*) FROM subscriptions") + public abstract Flowable rowCount(); + @Override @Query("DELETE FROM " + SUBSCRIPTION_TABLE) public abstract int deleteAll(); diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java index 1e69567e1..2500dfc71 100644 --- a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java @@ -19,7 +19,7 @@ import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCR indices = {@Index(value = {SUBSCRIPTION_SERVICE_ID, SUBSCRIPTION_URL}, unique = true)}) public class SubscriptionEntity { - final static String SUBSCRIPTION_UID = "uid"; + public final static String SUBSCRIPTION_UID = "uid"; final static String SUBSCRIPTION_TABLE = "subscriptions"; final static String SUBSCRIPTION_SERVICE_ID = "service_id"; final static String SUBSCRIPTION_URL = "url"; diff --git a/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java index d84fe0195..d208f92b3 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java +++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java @@ -269,11 +269,11 @@ public class HistoryRecordManager { for (LocalItem item : items) { long streamId; if (item instanceof StreamStatisticsEntry) { - streamId = ((StreamStatisticsEntry) item).streamId; + streamId = ((StreamStatisticsEntry) item).getStreamId(); } else if (item instanceof PlaylistStreamEntity) { streamId = ((PlaylistStreamEntity) item).getStreamUid(); } else if (item instanceof PlaylistStreamEntry) { - streamId = ((PlaylistStreamEntry) item).streamId; + streamId = ((PlaylistStreamEntry) item).getStreamId(); } else { result.add(null); continue; diff --git a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java index 31ae70954..a54c2a9a4 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java @@ -76,11 +76,11 @@ public class StatisticsPlaylistFragment switch (sortMode) { case LAST_PLAYED: Collections.sort(results, (left, right) -> - right.latestAccessDate.compareTo(left.latestAccessDate)); + right.getLatestAccessDate().compareTo(left.getLatestAccessDate())); return results; case MOST_PLAYED: Collections.sort(results, (left, right) -> - Long.compare(right.watchCount, left.watchCount)); + Long.compare(right.getWatchCount(), left.getWatchCount())); return results; default: return null; } @@ -153,9 +153,9 @@ public class StatisticsPlaylistFragment if (selectedItem instanceof StreamStatisticsEntry) { final StreamStatisticsEntry item = (StreamStatisticsEntry) selectedItem; NavigationHelper.openVideoDetailFragment(getFM(), - item.serviceId, - item.url, - item.title); + item.getStreamEntity().getServiceId(), + item.getStreamEntity().getUrl(), + item.getStreamEntity().getTitle()); } } @@ -402,7 +402,7 @@ public class StatisticsPlaylistFragment .get(index); if(infoItem instanceof StreamStatisticsEntry) { final StreamStatisticsEntry entry = (StreamStatisticsEntry) infoItem; - final Disposable onDelete = recordManager.deleteStreamHistory(entry.streamId) + final Disposable onDelete = recordManager.deleteStreamHistory(entry.getStreamId()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( howManyDeleted -> { diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java index 30cc6de32..7eef3e67e 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java @@ -52,12 +52,12 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder { if (!(localItem instanceof PlaylistStreamEntry)) return; final PlaylistStreamEntry item = (PlaylistStreamEntry) localItem; - itemVideoTitleView.setText(item.title); - itemAdditionalDetailsView.setText(Localization.concatenateStrings(item.uploader, - NewPipe.getNameOfService(item.serviceId))); + itemVideoTitleView.setText(item.getStreamEntity().getTitle()); + itemAdditionalDetailsView.setText(Localization.concatenateStrings(item.getStreamEntity().getUploader(), + NewPipe.getNameOfService(item.getStreamEntity().getServiceId()))); - if (item.duration > 0) { - itemDurationView.setText(Localization.getDurationString(item.duration)); + if (item.getStreamEntity().getDuration() > 0) { + itemDurationView.setText(Localization.getDurationString(item.getStreamEntity().getDuration())); itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), R.color.duration_background_color)); itemDurationView.setVisibility(View.VISIBLE); @@ -65,7 +65,7 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder { StreamStateEntity state = historyRecordManager.loadLocalStreamStateBatch(new ArrayList() {{ add(localItem); }}).blockingGet().get(0); if (state != null) { itemProgressView.setVisibility(View.VISIBLE); - itemProgressView.setMax((int) item.duration); + itemProgressView.setMax((int) item.getStreamEntity().getDuration()); itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime())); } else { itemProgressView.setVisibility(View.GONE); @@ -75,7 +75,7 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder { } // Default thumbnail is shown on error, while loading and if the url is empty - itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, + itemBuilder.displayImage(item.getStreamEntity().getThumbnailUrl(), itemThumbnailView, ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS); itemView.setOnClickListener(view -> { @@ -102,8 +102,8 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder { final PlaylistStreamEntry item = (PlaylistStreamEntry) localItem; StreamStateEntity state = historyRecordManager.loadLocalStreamStateBatch(new ArrayList() {{ add(localItem); }}).blockingGet().get(0); - if (state != null && item.duration > 0) { - itemProgressView.setMax((int) item.duration); + if (state != null && item.getStreamEntity().getDuration() > 0) { + itemProgressView.setMax((int) item.getStreamEntity().getDuration()); if (itemProgressView.getVisibility() == View.VISIBLE) { itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime())); } else { diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java index 75fbf13ea..77f947031 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java @@ -71,9 +71,9 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder { private String getStreamInfoDetailLine(final StreamStatisticsEntry entry, final DateFormat dateFormat) { final String watchCount = Localization.shortViewCount(itemBuilder.getContext(), - entry.watchCount); - final String uploadDate = dateFormat.format(entry.latestAccessDate); - final String serviceName = NewPipe.getNameOfService(entry.serviceId); + entry.getWatchCount()); + final String uploadDate = dateFormat.format(entry.getLatestAccessDate()); + final String serviceName = NewPipe.getNameOfService(entry.getStreamEntity().getServiceId()); return Localization.concatenateStrings(watchCount, uploadDate, serviceName); } @@ -82,11 +82,11 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder { if (!(localItem instanceof StreamStatisticsEntry)) return; final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem; - itemVideoTitleView.setText(item.title); - itemUploaderView.setText(item.uploader); + itemVideoTitleView.setText(item.getStreamEntity().getTitle()); + itemUploaderView.setText(item.getStreamEntity().getUploader()); - if (item.duration > 0) { - itemDurationView.setText(Localization.getDurationString(item.duration)); + if (item.getStreamEntity().getDuration() > 0) { + itemDurationView.setText(Localization.getDurationString(item.getStreamEntity().getDuration())); itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), R.color.duration_background_color)); itemDurationView.setVisibility(View.VISIBLE); @@ -94,7 +94,7 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder { StreamStateEntity state = historyRecordManager.loadLocalStreamStateBatch(new ArrayList() {{ add(localItem); }}).blockingGet().get(0); if (state != null) { itemProgressView.setVisibility(View.VISIBLE); - itemProgressView.setMax((int) item.duration); + itemProgressView.setMax((int) item.getStreamEntity().getDuration()); itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime())); } else { itemProgressView.setVisibility(View.GONE); @@ -109,7 +109,7 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder { } // Default thumbnail is shown on error, while loading and if the url is empty - itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, + itemBuilder.displayImage(item.getStreamEntity().getThumbnailUrl(), itemThumbnailView, ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS); itemView.setOnClickListener(view -> { @@ -133,8 +133,8 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder { final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem; StreamStateEntity state = historyRecordManager.loadLocalStreamStateBatch(new ArrayList() {{ add(localItem); }}).blockingGet().get(0); - if (state != null && item.duration > 0) { - itemProgressView.setMax((int) item.duration); + if (state != null && item.getStreamEntity().getDuration() > 0) { + itemProgressView.setMax((int) item.getStreamEntity().getDuration()); if (itemProgressView.getVisibility() == View.VISIBLE) { itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime())); } else { diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java index 17599a1ca..dd9958486 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java @@ -168,7 +168,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment streamIds = new ArrayList<>(items.size()); for (final LocalItem item : items) { if (item instanceof PlaylistStreamEntry) { - streamIds.add(((PlaylistStreamEntry) item).streamId); + streamIds.add(((PlaylistStreamEntry) item).getStreamId()); } } @@ -579,7 +579,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment NavigationHelper.playOnBackgroundPlayer(context, getPlayQueueStartingAt(item), true)); StreamDialogEntry.set_as_playlist_thumbnail.setCustomAction( - (fragment, infoItemDuplicate) -> changeThumbnailUrl(item.thumbnailUrl)); + (fragment, infoItemDuplicate) -> changeThumbnailUrl(item.getStreamEntity().getThumbnailUrl())); StreamDialogEntry.delete.setCustomAction( (fragment, infoItemDuplicate) -> deleteItem(item)); diff --git a/assets/db.dia b/assets/db.dia index 349163d775a129e881de40dd6a0ad7be898e458b..adcf8fb9b12d8806cb96e0c200ce8375c3a51390 100644 GIT binary patch literal 2880 zcmV-G3%~RqiwFP!000021MOW+lj1lMz4xym=qsf{{`N3c6T3UHF}I1>?L9jLR)MFD z&B~&xx$JLW87PWxjAc{U{fOwP0ff>{9`ehVsr0YE{g}p`S1z(BO`f`x^t&CNjM6Yl z#!ub6G@god+JW4oc;3n`1bZj;zh<}Do7m7NyZ=l z$KsehszHz4=P#Yk_5>l5EPrkNDw9$~!@1<0giZNVcgRNH$0D64VRtoZJ#Li7sp!12 z_^JE#W%=knuBUn2ndi*hGdAW!!P)oIQ}@-w_sdK@<6?i>=`776HAF6E2SZNGlmB;X z%z9L&1|{R?uYa??t`2Ga!pB_44)mlEIb~uTC5LvDZ{n3B4B{Dvp`2m+p5J#Zm%jVt zdUM0unu~91PTba9lm)X?NWmg`XmXgQF=xrjsuXix-cB}Rv4YsS)6zJLmq^O=!tuYb zI4g1jtCu&wwrhX6V-bbtf4Dm%PR#Nag>n-7sL5Tc*%vj<{u*V`Fy_aOK1yU|(=V!- ze*fE9uHGLX*F?i=wv(5K(Bn3@ufIHu=TXSB3ozRqb7H2+df4M7ukVj5>X_9_dl&1a ziI^?8SWo|l`s%nCi~Ks>S%1UpC8xtiGJ1V~w&j#`_!l3^_1XTeW*)MPcOE+5nEG({ zgN7&)5`|CQKl{7>+CM`zomvVH95zcgs4{!h-FLv@@^NbJ;c$Oyp$?xV5q@<}Sm#ljM<6$(C}w=hG`sbo5|`rsU%g@J`P^eL%-#(bzaysiWh1DPm?s8 zv5~rjWpa^U2Gs>@HN`*F?=Iq^Jk~E;&sdUe40{{PfAIK~%V@+N{;vF0?f}-Hx%LgIWNiG#?Tr3IVuhh+YulyQ8$ zj=~d~#+g9i)M}>=^qj>wO~#cOJ6uwWT_wucI=-qF3;VCPmdxXLNy#snI*8D(w>3|e zD?^o*js`lngY`_LW5Kg5kkOO_+Eqfk0=%^o+sB27$B&>}BMPUwQMlbR%o?NceEI=( ziRIWm(;$9xqww;MDiVcLBX?1ak+)C$O1Kvy?@A-@{(k6f)D?PXh`JGVBkD%fji~#6 zqwYE4ZN%GEp42CY+Zh+H(TLwtMpA?wp*Ccsn%8L&-we?IUj7B9f$4fNEh8Copc<%d z6{^>HtbF!cVq4k#R;5Yc8~8r@t%q~LWRxZd8v*M!5$su{z=2@kTT5+d}*I zDvbfy%>s6vWs!z%S)}3h45!Ar$mRS4(;*JEdxlT!<`zb3xT;8Hq?fy>f(p4pRgz}e zF9KzeP1}$-i`gQMGKsQCltrQ}5@nGni$qx@$|6w~c?&$}??XR=_K(aK&<%9ot%_t; zpdwMPUKNRY8x_fFa+50!SKXb8vBMEnhP36d#54NzknIWUbh0MYo zE^@eS%HiVXTZqJ4C^F@|I*1h0`+;`gSgq?`t?Sg6Y+ZfH?NM5zw3JUjp!RZOfcj%L zS5_+TsJq)2U=edSv~VFX{Umq4=1hzx!9>L>saQZ-g0uu_3DOdzrTdkZ5PKs=X;bJu zQa{i9J^^U(2wAChwM)3m9B|!AT<1|EN=*Ul#@FLH+VyxwkGgf%;^p%Xs81}XXY?Cg zhgaTH#cJ>@2UT|vMfF9|HzuKpLKB513QZK6XcIJ1lqEqLZDE+rMkAhO5Jmy)mH}Jv z8D|oEI-w!8LPK(OHKYgX_YAW}5ou*$@Zb}}%y)A(xF4^MuZqY>$~jIIbQleJo2xPP z_eCb7hRVD7FdK6N%Q~#&jmze~a2_IwizF_RxJcq6iHn^ANaZ4xdvm#I8IS|p zz;@G{nELt!)h;m{*Y44JtEx9v1`o7Pd`CsI)M@e_eQn1T3CbN}_N{Fm;bPxDqVS5M z@L(*``3$i*VsXUch{X|$qo5j*3Zifn=^-7CQjaq2rboAxl(0phT2Sj>6WUSYLHqie zlDDc?$p6-=Ds9=xx9od;*X|jV&?d`D+P*`z2Xvh)xilAUZ*Gx?j<0 zYw3pk1o;U?Aiy@T{izZAzxWFmJQ*F0+NpNbzG``A63ebLW?xw`6ce!Q!NwOL!dOFN+LH z-{7DS>DjU}@Cc=jMh}a7AU#LDc5;|OiFuaJY+O=!pjjj9DadTjeGV!QNKa!)j{(wi zk&N7k^MLZyMtS_Y9}rycTtd<*ZXO z1AUu!L3)m!#gTCyG0nomF6Fki9f?{V&>qkp&>qkp(4JDX$I?h_(a%h5Il8n*U5*X6 zcGk0HWng$sCp<@ec4U}KsA*R?PRntqY}4AI_jdM?)`SuoiRDVZEtYfn>O{* zor@Td2Bg~r>ES|M9D*=?OX8G;9Q~nz?l#@0Oh!|lNj9AU*T8j)aQ!_>LQoNa`hIS} ew|XA41sBg>R!`Kwv0&5ZFaHO;N_ZC!dT z{{J+J=)G!mKm7V>FnHTRNF~i)+rCPr6nwmtY!K5qdl-)CP?!~1PF_ql$K^u3@{HWrM2FYj7Y58toT^@550Zs&_6<*JEXEecIa`pN!& z(`MT$RgL25*U!INpEsAZec`pQLl;^KBIi_0d0fO%wuv@|FoaT=p(bJ0F~lQ}q6!dsue-)woM3=P zbS?kT>w)e0&^Zq}U5?X<;0vinn)IMEeGZcPHO^Tq%fK4<|1x!!;2d+8&Z04$Di3zr zHH?}om{>DmA-M9ISHX9-ItrF|Vb8%DCsFtoChh8`;zR`iQSg`)?UubZc%*Y4t<+$i z#7VlK6E)GLa+OV%YMR@0@lW-`<4n{i`)Tx)#_0~&+adqXq9-Q#gns#lvRAze*plYl zYbajAALEGsZ`u57I63-j#M>KatcJF)0J%5e)ScemTKjUSyXWaCe?(@!T(E0jbooyT z9(!Y`@?uLgk0XIX**K>z@fcQ=#M9Qmii2sj`JoQh;qkPZEO0(ryo{o?%YF)~5W%01 zwT#zeLW?(bFb=vLpHn8D_=E))q_uM9d(Y|?sCI?4J_qTYdA$_T#R&c1^}paWINdF$ zv-d%2kh)i-rcYE-5n#Lqwfjcxt++R4BA6u07QJ2sj-dDZ(>r84=^8^fxP1|BA5L-K zzov5=%zU{boF41h?@;c1#?jem#lX#l$-uc9>k9-aJ-BwIkCBN$u&$8Z1 z(O!r7U4yuJf!{!W_k-kjr2?D>sicx2y^HiN(z{6SBE5UP>D}#kjU+FUynPDQ7b2Mo zmZpK^7fc~QhC-;`O{i`(4%IEBsPL~;qhO*&;iJ?jEF!v!cq&n(fFcDHDWFIJMG7cVK#>B96tLY8B5~+U z(3!yQ53e&>O?4)d=uWnf!y%sKn_cQmdq|nijw+Ktbgw~1-kW>x|3>Rs-0m@VvY$wLQ6t$7gMLHMhT%>c6&b{7r?n~(njv_!>w^wOh9I%1lyob6| z=2r(?d3ryvqbEk|u1D)OB&YwdML->$OI3!UtuBGwvg9VMT`l{crv)D0{%0XPseTO~^iRZ8gnyDe>fyMy8sH z&CC^Oa`5O?Xgp;mEN`m35Zm*O6HR@`aL#Jl=oH*4K21fx<=E%o)4->JPt#+cMk7Rr zTXXX|kDhmH{01_Q(Y8laR%Y!t->cnqEYr?Tb=UN|`y^G|t38L5?V)A5zIA6<#H@{c z9ecI6V8W~0LrZ-t`I*y2<@E4@>*g%t zn4X?Vg05$GSvHpg$~Ldi{OqYkjJmj2v3W%c_N%seh2>zo3W%lG=*pi(e&ep zrdZGq?X-t+IGs#bnnF1Rv3o{r!4`~4Y>*0(>83=cV`~|7y@l;6HiC~q^iGAQ9gs$R z-SbDLMl9QKGGO1dZ+g3mg9bd>((p~c%JPJBnr`g+Ay+bBzX~j6z)}V*&%`>0cGfXy zEpAxhUuq?TZZtj)+_~%Y3~<%`$g+v+-s#F&xZyLuYul+xO3f#((}prwDZyqdm#hsi zsENVy^9_AV*6;w2!}hE&_zJ>jWDWgtY`UhM~u!!<$a zo>6+dQjgy?q*gTKuiixkE WHc!-_si5<(pZ*5+VQC&K{Qv;aZ3ZF$