From 59dd6e471266a2de86500e7cc719818dadc0a207 Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Mon, 23 Mar 2026 16:13:36 +0100 Subject: [PATCH] feat: allow filtering out paid feed streams This commit adds a new "Members only" filter to the feed page. To achieve this, a database migration is required to save the content availability of streams. Closes #12883 Relates to #12011 Relates to #12040 --- .../10.json | 706 ++++++++++++++++++ .../org/schabi/newpipe/NewPipeDatabase.kt | 4 +- .../schabi/newpipe/database/AppDatabase.kt | 2 +- .../org/schabi/newpipe/database/Migrations.kt | 17 + .../newpipe/database/feed/dao/FeedDAO.kt | 24 +- .../database/stream/model/StreamEntity.kt | 12 +- .../newpipe/local/feed/FeedDatabaseManager.kt | 6 +- .../schabi/newpipe/local/feed/FeedFragment.kt | 7 +- .../newpipe/local/feed/FeedViewModel.kt | 44 +- app/src/main/res/values/settings_keys.xml | 1 + app/src/main/res/values/strings.xml | 1 + 11 files changed, 798 insertions(+), 26 deletions(-) create mode 100644 app/schemas/org.schabi.newpipe.database.AppDatabase/10.json diff --git a/app/schemas/org.schabi.newpipe.database.AppDatabase/10.json b/app/schemas/org.schabi.newpipe.database.AppDatabase/10.json new file mode 100644 index 00000000000..69c916b3b2a --- /dev/null +++ b/app/schemas/org.schabi.newpipe.database.AppDatabase/10.json @@ -0,0 +1,706 @@ +{ + "formatVersion": 1, + "database": { + "version": 10, + "identityHash": "f740cab09d3a0afd1578274b8ecf9dc2", + "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, `notification_mode` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "service_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "avatarUrl", + "columnName": "avatar_url", + "affinity": "TEXT" + }, + { + "fieldPath": "subscriberCount", + "columnName": "subscriber_count", + "affinity": "INTEGER" + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT" + }, + { + "fieldPath": "notificationMode", + "columnName": "notification_mode", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uid" + ] + }, + "indices": [ + { + "name": "index_subscriptions_service_id_url", + "unique": true, + "columnNames": [ + "service_id", + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_subscriptions_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)" + } + ] + }, + { + "tableName": "search_history", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`creation_date` INTEGER, `service_id` INTEGER NOT NULL, `search` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "creationDate", + "columnName": "creation_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "serviceId", + "columnName": "service_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "search", + "columnName": "search", + "affinity": "TEXT" + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_search_history_search", + "unique": false, + "columnNames": [ + "search" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_search_history_search` ON `${TABLE_NAME}` (`search`)" + } + ] + }, + { + "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, `uploader_url` TEXT, `thumbnail_url` TEXT, `availability` TEXT NOT NULL, `view_count` INTEGER, `textual_upload_date` TEXT, `upload_date` INTEGER, `is_upload_date_approximation` 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": "uploaderUrl", + "columnName": "uploader_url", + "affinity": "TEXT" + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnail_url", + "affinity": "TEXT" + }, + { + "fieldPath": "contentAvailability", + "columnName": "availability", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "viewCount", + "columnName": "view_count", + "affinity": "INTEGER" + }, + { + "fieldPath": "textualUploadDate", + "columnName": "textual_upload_date", + "affinity": "TEXT" + }, + { + "fieldPath": "uploadDate", + "columnName": "upload_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "isUploadDateApproximation", + "columnName": "is_upload_date_approximation", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uid" + ] + }, + "indices": [ + { + "name": "index_streams_service_id_url", + "unique": true, + "columnNames": [ + "service_id", + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_streams_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)" + } + ] + }, + { + "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": { + "autoGenerate": false, + "columnNames": [ + "stream_id", + "access_date" + ] + }, + "indices": [ + { + "name": "index_stream_history_stream_id", + "unique": false, + "columnNames": [ + "stream_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `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": "progressMillis", + "columnName": "progress_time", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "stream_id" + ] + }, + "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, `is_thumbnail_permanent` INTEGER NOT NULL, `thumbnail_stream_id` INTEGER NOT NULL, `display_index` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "isThumbnailPermanent", + "columnName": "is_thumbnail_permanent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "thumbnailStreamId", + "columnName": "thumbnail_stream_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayIndex", + "columnName": "display_index", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uid" + ] + } + }, + { + "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": { + "autoGenerate": false, + "columnNames": [ + "playlist_id", + "join_index" + ] + }, + "indices": [ + { + "name": "index_playlist_stream_join_playlist_id_join_index", + "unique": true, + "columnNames": [ + "playlist_id", + "join_index" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `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" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `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, `display_index` INTEGER NOT NULL, `stream_count` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "service_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "orderingName", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT" + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnail_url", + "affinity": "TEXT" + }, + { + "fieldPath": "uploader", + "columnName": "uploader", + "affinity": "TEXT" + }, + { + "fieldPath": "displayIndex", + "columnName": "display_index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "streamCount", + "columnName": "stream_count", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uid" + ] + }, + "indices": [ + { + "name": "index_remote_playlists_service_id_url", + "unique": true, + "columnNames": [ + "service_id", + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_remote_playlists_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)" + } + ] + }, + { + "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": { + "autoGenerate": false, + "columnNames": [ + "stream_id", + "subscription_id" + ] + }, + "indices": [ + { + "name": "index_feed_subscription_id", + "unique": false, + "columnNames": [ + "subscription_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `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, `sort_order` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortOrder", + "columnName": "sort_order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uid" + ] + }, + "indices": [ + { + "name": "index_feed_group_sort_order", + "unique": false, + "columnNames": [ + "sort_order" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_sort_order` ON `${TABLE_NAME}` (`sort_order`)" + } + ] + }, + { + "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": { + "autoGenerate": false, + "columnNames": [ + "group_id", + "subscription_id" + ] + }, + "indices": [ + { + "name": "index_feed_group_subscription_join_subscription_id", + "unique": false, + "columnNames": [ + "subscription_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `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" + ] + } + ] + }, + { + "tableName": "feed_last_updated", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`subscription_id` INTEGER NOT NULL, `last_updated` INTEGER, PRIMARY KEY(`subscription_id`), FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "subscriptionId", + "columnName": "subscription_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "last_updated", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "subscription_id" + ] + }, + "foreignKeys": [ + { + "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, 'f740cab09d3a0afd1578274b8ecf9dc2')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.kt b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.kt index 6527bd2ae88..c11ac822133 100644 --- a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.kt +++ b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.kt @@ -18,6 +18,7 @@ import org.schabi.newpipe.database.Migrations.MIGRATION_5_6 import org.schabi.newpipe.database.Migrations.MIGRATION_6_7 import org.schabi.newpipe.database.Migrations.MIGRATION_7_8 import org.schabi.newpipe.database.Migrations.MIGRATION_8_9 +import org.schabi.newpipe.database.Migrations.MIGRATION_9_10 object NewPipeDatabase { @@ -37,7 +38,8 @@ object NewPipeDatabase { MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, - MIGRATION_8_9 + MIGRATION_8_9, + MIGRATION_9_10 ).build() } diff --git a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.kt b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.kt index 286eddf7b76..7c7a10e772a 100644 --- a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.kt +++ b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.kt @@ -34,7 +34,7 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity @TypeConverters(Converters::class) @Database( - version = Migrations.DB_VER_9, + version = Migrations.DB_VER_10, entities = [ SubscriptionEntity::class, SearchHistoryEntry::class, diff --git a/app/src/main/java/org/schabi/newpipe/database/Migrations.kt b/app/src/main/java/org/schabi/newpipe/database/Migrations.kt index 414f7489390..e15f7d1f73d 100644 --- a/app/src/main/java/org/schabi/newpipe/database/Migrations.kt +++ b/app/src/main/java/org/schabi/newpipe/database/Migrations.kt @@ -29,6 +29,7 @@ object Migrations { const val DB_VER_7 = 7 const val DB_VER_8 = 8 const val DB_VER_9 = 9 + const val DB_VER_10 = 10 private val TAG = Migrations::class.java.getName() private val isDebug = MainActivity.DEBUG @@ -348,4 +349,20 @@ object Migrations { db.endTransaction() } } + + val MIGRATION_9_10 = Migration(DB_VER_9, DB_VER_10) { db -> + try { + db.beginTransaction() + + // Create a new column content_availability + db.execSQL( + "ALTER TABLE `streams` ADD COLUMN `availability` " + + "TEXT NOT NULL DEFAULT 'UNKNOWN'" + ) + + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + } } 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 index 5861fa767f1..8c80c7cd6f4 100644 --- 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 @@ -23,12 +23,16 @@ abstract class FeedDAO { abstract fun deleteAll(): Int /** - * @param groupId the group id to get feed streams of; use - * [FeedGroupEntity.GROUP_ALL_ID] to not filter by group - * @param includePlayed if false, only return all of the live, never-played or non-finished - * feed streams (see `@see` items); if true no filter is applied - * @param uploadDateBefore get only streams uploaded before this date (useful to filter out - * future streams); use null to not filter by upload date + * @param groupId the group id to get feed streams of; use + * [FeedGroupEntity.GROUP_ALL_ID] to not filter by group + * @param includePlayed if false, only return all of the live, non-finished + * feed streams (see `@see` items); if true no filter is applied + * @param includePartiallyPlayed if false, only return all of the never-played + * feed streams (see `@see` items); if true no filter is applied + * @param uploadDateBefore get only streams uploaded before this date (useful to filter out + * future streams); use null to not filter by upload date + * @param includeMembersOnly if false, only return feed streams that are publicly accessible; + * if true no filter is applied * @return the feed streams filtered according to the conditions provided in the parameters * @see StreamStateEntity.isFinished() * @see StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS @@ -81,6 +85,11 @@ abstract class FeedDAO { OR s.upload_date IS NULL OR s.upload_date < :uploadDateBefore ) + AND ( + :includeMembersOnly + OR (s.availability <> 'MEMBERSHIP' + AND s.availability <> 'PAID') + ) ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC LIMIT 500 @@ -90,7 +99,8 @@ abstract class FeedDAO { groupId: Long, includePlayed: Boolean, includePartiallyPlayed: Boolean, - uploadDateBefore: OffsetDateTime? + uploadDateBefore: OffsetDateTime?, + includeMembersOnly: Boolean ): Maybe> /** 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 index 067f666b634..94d754749a6 100644 --- 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 @@ -11,6 +11,7 @@ import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_SE 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.ContentAvailability import org.schabi.newpipe.extractor.stream.StreamInfo import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.extractor.stream.StreamType @@ -52,6 +53,9 @@ data class StreamEntity( @ColumnInfo(name = STREAM_THUMBNAIL_URL) var thumbnailUrl: String? = null, + @ColumnInfo(name = STREAM_AVAILABILITY) + var contentAvailability: ContentAvailability = ContentAvailability.UNKNOWN, + @ColumnInfo(name = STREAM_VIEWS) var viewCount: Long? = null, @@ -69,7 +73,8 @@ data class StreamEntity( serviceId = item.serviceId, url = item.url, title = item.name, streamType = item.streamType, duration = item.duration, uploader = item.uploaderName, uploaderUrl = item.uploaderUrl, - thumbnailUrl = ImageStrategy.imageListToDbUrl(item.thumbnails), viewCount = item.viewCount, + thumbnailUrl = ImageStrategy.imageListToDbUrl(item.thumbnails), + contentAvailability = item.contentAvailability, viewCount = item.viewCount, textualUploadDate = item.textualUploadDate, uploadDate = item.uploadDate?.offsetDateTime(), isUploadDateApproximation = item.uploadDate?.isApproximation ) @@ -79,7 +84,8 @@ data class StreamEntity( serviceId = info.serviceId, url = info.url, title = info.name, streamType = info.streamType, duration = info.duration, uploader = info.uploaderName, uploaderUrl = info.uploaderUrl, - thumbnailUrl = ImageStrategy.imageListToDbUrl(info.thumbnails), viewCount = info.viewCount, + thumbnailUrl = ImageStrategy.imageListToDbUrl(info.thumbnails), + contentAvailability = info.contentAvailability, viewCount = info.viewCount, textualUploadDate = info.textualUploadDate, uploadDate = info.uploadDate?.offsetDateTime(), isUploadDateApproximation = info.uploadDate?.isApproximation ) @@ -102,6 +108,7 @@ data class StreamEntity( item.uploaderName = uploader item.uploaderUrl = uploaderUrl item.thumbnails = ImageStrategy.dbUrlToImageList(thumbnailUrl) + item.contentAvailability = contentAvailability if (viewCount != null) item.viewCount = viewCount as Long item.textualUploadDate = textualUploadDate @@ -123,6 +130,7 @@ data class StreamEntity( const val STREAM_UPLOADER = "uploader" const val STREAM_UPLOADER_URL = "uploader_url" const val STREAM_THUMBNAIL_URL = "thumbnail_url" + const val STREAM_AVAILABILITY = "availability" const val STREAM_VIEWS = "view_count" const val STREAM_TEXTUAL_UPLOAD_DATE = "textual_upload_date" diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt index 3e3a47f57e4..1a3c3f09a50 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt @@ -44,13 +44,15 @@ class FeedDatabaseManager(context: Context) { groupId: Long, includePlayedStreams: Boolean, includePartiallyPlayedStreams: Boolean, - includeFutureStreams: Boolean + includeFutureStreams: Boolean, + includeMembersOnlyStreams: Boolean ): Maybe> { return feedTable.getStreams( groupId, includePlayedStreams, includePartiallyPlayedStreams, - if (includeFutureStreams) null else OffsetDateTime.now() + if (includeFutureStreams) null else OffsetDateTime.now(), + includeMembersOnlyStreams ) } diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt index 89b89a80001..e497a8b2ec9 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt @@ -246,13 +246,15 @@ class FeedFragment : BaseStateFragment() { val dialogItems = arrayOf( getString(R.string.feed_show_watched), getString(R.string.feed_show_partially_watched), - getString(R.string.feed_show_upcoming) + getString(R.string.feed_show_upcoming), + getString(R.string.feed_show_members_only) ) val checkedDialogItems = booleanArrayOf( viewModel.getShowPlayedItemsFromPreferences(), viewModel.getShowPartiallyPlayedItemsFromPreferences(), - viewModel.getShowFutureItemsFromPreferences() + viewModel.getShowFutureItemsFromPreferences(), + viewModel.getShowMembersOnlyItemsFromPreferences() ) AlertDialog.Builder(requireContext()) @@ -264,6 +266,7 @@ class FeedFragment : BaseStateFragment() { viewModel.setSaveShowPlayedItems(checkedDialogItems[0]) viewModel.setSaveShowPartiallyPlayedItems(checkedDialogItems[1]) viewModel.setSaveShowFutureItems(checkedDialogItems[2]) + viewModel.setSaveShowMembersOnlyItems(checkedDialogItems[3]) } .setNegativeButton(R.string.cancel, null) .show() diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt index 19adf6eaada..e1696e61c4e 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt @@ -11,7 +11,7 @@ import androidx.lifecycle.viewmodel.viewModelFactory import androidx.preference.PreferenceManager import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Flowable -import io.reactivex.rxjava3.functions.Function6 +import io.reactivex.rxjava3.functions.Function7 import io.reactivex.rxjava3.processors.BehaviorProcessor import io.reactivex.rxjava3.schedulers.Schedulers import java.time.OffsetDateTime @@ -33,7 +33,8 @@ class FeedViewModel( groupId: Long = FeedGroupEntity.GROUP_ALL_ID, initialShowPlayedItems: Boolean, initialShowPartiallyPlayedItems: Boolean, - initialShowFutureItems: Boolean + initialShowFutureItems: Boolean, + initialShowMembersOnlyItems: Boolean ) : ViewModel() { private val feedDatabaseManager = FeedDatabaseManager(application) @@ -52,6 +53,11 @@ class FeedViewModel( .startWithItem(initialShowFutureItems) .distinctUntilChanged() + private val showMembersOnlyItems = BehaviorProcessor.create() + private val showMembersOnlyItemsFlowable = showMembersOnlyItems + .startWithItem(initialShowMembersOnlyItems) + .distinctUntilChanged() + private val mutableStateLiveData = MutableLiveData() val stateLiveData: LiveData = mutableStateLiveData @@ -61,27 +67,29 @@ class FeedViewModel( showPlayedItemsFlowable, showPartiallyPlayedItemsFlowable, showFutureItemsFlowable, + showMembersOnlyItemsFlowable, feedDatabaseManager.notLoadedCount(groupId), feedDatabaseManager.oldestSubscriptionUpdate(groupId), - Function6 { + Function7 { t1: FeedEventManager.Event, t2: Boolean, t3: Boolean, t4: Boolean, - t5: Long, - t6: List + t5: Boolean, + t6: Long, + t7: List -> - return@Function6 CombineResultEventHolder(t1, t2, t3, t4, t5, t6.firstOrNull()) + return@Function7 CombineResultEventHolder(t1, t2, t3, t4, t5, t6, t7.firstOrNull()) } ) .throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS) .subscribeOn(Schedulers.io()) .observeOn(Schedulers.io()) - .map { (event, showPlayedItems, showPartiallyPlayedItems, showFutureItems, notLoadedCount, oldestUpdate) -> + .map { (event, showPlayedItems, showPartiallyPlayedItems, showFutureItems, showMembersOnlyItems, notLoadedCount, oldestUpdate) -> val streamItems = if (event is SuccessResultEvent || event is IdleEvent) { feedDatabaseManager - .getStreams(groupId, showPlayedItems, showPartiallyPlayedItems, showFutureItems) + .getStreams(groupId, showPlayedItems, showPartiallyPlayedItems, showFutureItems, showMembersOnlyItems) .blockingGet(arrayListOf()) } else { arrayListOf() @@ -115,8 +123,9 @@ class FeedViewModel( val t2: Boolean, val t3: Boolean, val t4: Boolean, - val t5: Long, - val t6: OffsetDateTime? + val t5: Boolean, + val t6: Long, + val t7: OffsetDateTime? ) private data class CombineResultDataHolder( @@ -153,6 +162,15 @@ class FeedViewModel( fun getShowFutureItemsFromPreferences() = getShowFutureItemsFromPreferences(application) + fun setSaveShowMembersOnlyItems(showMembersOnlyItems: Boolean) { + this.showMembersOnlyItems.onNext(showMembersOnlyItems) + PreferenceManager.getDefaultSharedPreferences(application).edit { + putBoolean(application.getString(R.string.feed_show_members_only_items_key), showMembersOnlyItems) + } + } + + fun getShowMembersOnlyItemsFromPreferences() = getShowMembersOnlyItemsFromPreferences(application) + companion object { private fun getShowPlayedItemsFromPreferences(context: Context) = PreferenceManager.getDefaultSharedPreferences(context) .getBoolean(context.getString(R.string.feed_show_watched_items_key), true) @@ -163,6 +181,9 @@ class FeedViewModel( private fun getShowFutureItemsFromPreferences(context: Context) = PreferenceManager.getDefaultSharedPreferences(context) .getBoolean(context.getString(R.string.feed_show_future_items_key), true) + private fun getShowMembersOnlyItemsFromPreferences(context: Context) = PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean(context.getString(R.string.feed_show_members_only_items_key), true) + fun getFactory(context: Context, groupId: Long) = viewModelFactory { initializer { FeedViewModel( @@ -171,7 +192,8 @@ class FeedViewModel( // Read initial value from preferences getShowPlayedItemsFromPreferences(context.applicationContext), getShowPartiallyPlayedItemsFromPreferences(context.applicationContext), - getShowFutureItemsFromPreferences(context.applicationContext) + getShowFutureItemsFromPreferences(context.applicationContext), + getShowMembersOnlyItemsFromPreferences(context.applicationContext) ) } } diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index d01709d27ba..e0daf8df537 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -364,6 +364,7 @@ feed_show_played_items feed_show_partially_watched_items feed_show_future_items + feed_show_members_only_items show_thumbnail_key diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3ec84bfffbb..7b51fa6d36a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -829,6 +829,7 @@ Fully watched Partially watched Upcoming + Members only Sort ExoPlayer settings Manage some ExoPlayer settings. These changes require a player restart to take effect