diff --git a/app/schemas/org.schabi.newpipe.database.AppDatabase/9.json b/app/schemas/org.schabi.newpipe.database.AppDatabase/9.json index aced06c0a..b9a618638 100644 --- a/app/schemas/org.schabi.newpipe.database.AppDatabase/9.json +++ b/app/schemas/org.schabi.newpipe.database.AppDatabase/9.json @@ -458,7 +458,7 @@ "notNull": true }, { - "fieldPath": "name", + "fieldPath": "orderingName", "columnName": "name", "affinity": "TEXT", "notNull": false diff --git a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java deleted file mode 100644 index 21c5354f4..000000000 --- a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java +++ /dev/null @@ -1,72 +0,0 @@ -package org.schabi.newpipe; - -import static org.schabi.newpipe.database.AppDatabase.DATABASE_NAME; -import static org.schabi.newpipe.database.Migrations.MIGRATION_1_2; -import static org.schabi.newpipe.database.Migrations.MIGRATION_2_3; -import static org.schabi.newpipe.database.Migrations.MIGRATION_3_4; -import static org.schabi.newpipe.database.Migrations.MIGRATION_4_5; -import static org.schabi.newpipe.database.Migrations.MIGRATION_5_6; -import static org.schabi.newpipe.database.Migrations.MIGRATION_6_7; -import static org.schabi.newpipe.database.Migrations.MIGRATION_7_8; -import static org.schabi.newpipe.database.Migrations.MIGRATION_8_9; - -import android.content.Context; -import android.database.Cursor; - -import androidx.annotation.NonNull; -import androidx.room.Room; - -import org.schabi.newpipe.database.AppDatabase; - -public final class NewPipeDatabase { - private static volatile AppDatabase databaseInstance; - - private NewPipeDatabase() { - //no instance - } - - private static AppDatabase getDatabase(final Context context) { - return Room - .databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME) - .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, - MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9) - .build(); - } - - @NonNull - public static AppDatabase getInstance(@NonNull final Context context) { - AppDatabase result = databaseInstance; - if (result == null) { - synchronized (NewPipeDatabase.class) { - result = databaseInstance; - if (result == null) { - databaseInstance = getDatabase(context); - result = databaseInstance; - } - } - } - - return result; - } - - public static void checkpoint() { - if (databaseInstance == null) { - throw new IllegalStateException("database is not initialized"); - } - final Cursor c = databaseInstance.query("pragma wal_checkpoint(full)", null); - if (c.moveToFirst() && c.getInt(0) == 1) { - throw new RuntimeException("Checkpoint was blocked from completing"); - } - } - - public static void close() { - if (databaseInstance != null) { - synchronized (NewPipeDatabase.class) { - if (databaseInstance != null) { - databaseInstance.close(); - databaseInstance = null; - } - } - } - } -} diff --git a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.kt b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.kt new file mode 100644 index 000000000..c3ce51524 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.kt @@ -0,0 +1,80 @@ +/* + * SPDX-FileCopyrightText: 2017-2024 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe + +import android.content.Context +import androidx.room.Room.databaseBuilder +import org.schabi.newpipe.database.AppDatabase +import org.schabi.newpipe.database.Migrations.MIGRATION_1_2 +import org.schabi.newpipe.database.Migrations.MIGRATION_2_3 +import org.schabi.newpipe.database.Migrations.MIGRATION_3_4 +import org.schabi.newpipe.database.Migrations.MIGRATION_4_5 +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 kotlin.concurrent.Volatile + +object NewPipeDatabase { + + @Volatile + private var databaseInstance: AppDatabase? = null + + private fun getDatabase(context: Context): AppDatabase { + return databaseBuilder( + context.applicationContext, + AppDatabase::class.java, + AppDatabase.Companion.DATABASE_NAME + ).addMigrations( + MIGRATION_1_2, + MIGRATION_2_3, + MIGRATION_3_4, + MIGRATION_4_5, + MIGRATION_5_6, + MIGRATION_6_7, + MIGRATION_7_8, + MIGRATION_8_9 + ).build() + } + + @JvmStatic + fun getInstance(context: Context): AppDatabase { + var result = databaseInstance + if (result == null) { + synchronized(NewPipeDatabase::class.java) { + result = databaseInstance + if (result == null) { + databaseInstance = getDatabase(context) + result = databaseInstance + } + } + } + + return result!! + } + + @JvmStatic + fun checkpoint() { + checkNotNull(databaseInstance) { "database is not initialized" } + val c = databaseInstance!!.query("pragma wal_checkpoint(full)", null) + if (c.moveToFirst() && c.getInt(0) == 1) { + throw RuntimeException("Checkpoint was blocked from completing") + } + } + + @JvmStatic + fun close() { + if (databaseInstance != null) { + synchronized(NewPipeDatabase::class.java) { + if (databaseInstance != null) { + databaseInstance!!.close() + databaseInstance = null + } + } + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java deleted file mode 100644 index 04d93a238..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.schabi.newpipe.database; - -import static org.schabi.newpipe.database.Migrations.DB_VER_9; - -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.feed.model.FeedLastUpdatedEntity; -import org.schabi.newpipe.database.history.dao.SearchHistoryDAO; -import org.schabi.newpipe.database.history.dao.StreamHistoryDAO; -import org.schabi.newpipe.database.history.model.SearchHistoryEntry; -import org.schabi.newpipe.database.history.model.StreamHistoryEntity; -import org.schabi.newpipe.database.playlist.dao.PlaylistDAO; -import org.schabi.newpipe.database.playlist.dao.PlaylistRemoteDAO; -import org.schabi.newpipe.database.playlist.dao.PlaylistStreamDAO; -import org.schabi.newpipe.database.playlist.model.PlaylistEntity; -import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; -import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; -import org.schabi.newpipe.database.stream.dao.StreamDAO; -import org.schabi.newpipe.database.stream.dao.StreamStateDAO; -import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.database.stream.model.StreamStateEntity; -import org.schabi.newpipe.database.subscription.SubscriptionDAO; -import org.schabi.newpipe.database.subscription.SubscriptionEntity; - -@TypeConverters({Converters.class}) -@Database( - entities = { - SubscriptionEntity.class, SearchHistoryEntry.class, - StreamEntity.class, StreamHistoryEntity.class, StreamStateEntity.class, - PlaylistEntity.class, PlaylistStreamEntity.class, PlaylistRemoteEntity.class, - FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class, - FeedLastUpdatedEntity.class - }, - version = DB_VER_9 -) -public abstract class AppDatabase extends RoomDatabase { - public static final String DATABASE_NAME = "newpipe.db"; - - 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/AppDatabase.kt b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.kt new file mode 100644 index 000000000..286eddf7b --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.kt @@ -0,0 +1,68 @@ +/* + * SPDX-FileCopyrightText: 2017-2024 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database + +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.feed.model.FeedLastUpdatedEntity +import org.schabi.newpipe.database.history.dao.SearchHistoryDAO +import org.schabi.newpipe.database.history.dao.StreamHistoryDAO +import org.schabi.newpipe.database.history.model.SearchHistoryEntry +import org.schabi.newpipe.database.history.model.StreamHistoryEntity +import org.schabi.newpipe.database.playlist.dao.PlaylistDAO +import org.schabi.newpipe.database.playlist.dao.PlaylistRemoteDAO +import org.schabi.newpipe.database.playlist.dao.PlaylistStreamDAO +import org.schabi.newpipe.database.playlist.model.PlaylistEntity +import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity +import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity +import org.schabi.newpipe.database.stream.dao.StreamDAO +import org.schabi.newpipe.database.stream.dao.StreamStateDAO +import org.schabi.newpipe.database.stream.model.StreamEntity +import org.schabi.newpipe.database.stream.model.StreamStateEntity +import org.schabi.newpipe.database.subscription.SubscriptionDAO +import org.schabi.newpipe.database.subscription.SubscriptionEntity + +@TypeConverters(Converters::class) +@Database( + version = Migrations.DB_VER_9, + entities = [ + SubscriptionEntity::class, + SearchHistoryEntry::class, + StreamEntity::class, + StreamHistoryEntity::class, + StreamStateEntity::class, + PlaylistEntity::class, + PlaylistStreamEntity::class, + PlaylistRemoteEntity::class, + FeedEntity::class, + FeedGroupEntity::class, + FeedGroupSubscriptionEntity::class, + FeedLastUpdatedEntity::class + ] +) +abstract class AppDatabase : RoomDatabase() { + abstract fun feedDAO(): FeedDAO + abstract fun feedGroupDAO(): FeedGroupDAO + abstract fun playlistDAO(): PlaylistDAO + abstract fun playlistRemoteDAO(): PlaylistRemoteDAO + abstract fun playlistStreamDAO(): PlaylistStreamDAO + abstract fun searchHistoryDAO(): SearchHistoryDAO + abstract fun streamDAO(): StreamDAO + abstract fun streamHistoryDAO(): StreamHistoryDAO + abstract fun streamStateDAO(): StreamStateDAO + abstract fun subscriptionDAO(): SubscriptionDAO + + companion object { + const val DATABASE_NAME: String = "newpipe.db" + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java b/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java deleted file mode 100644 index 255f5ba8d..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.schabi.newpipe.database; - -import androidx.room.Dao; -import androidx.room.Delete; -import androidx.room.Insert; -import androidx.room.Update; - -import java.util.Collection; -import java.util.List; - -import io.reactivex.rxjava3.core.Flowable; - -@Dao -public interface BasicDAO { - /* Inserts */ - @Insert - long insert(Entity entity); - - @Insert - List insertAll(Collection entities); - - /* Searches */ - Flowable> getAll(); - - Flowable> listByService(int serviceId); - - /* Deletes */ - @Delete - void delete(Entity entity); - - int deleteAll(); - - /* Updates */ - @Update - int update(Entity entity); - - @Update - void update(Collection entities); -} diff --git a/app/src/main/java/org/schabi/newpipe/database/BasicDAO.kt b/app/src/main/java/org/schabi/newpipe/database/BasicDAO.kt new file mode 100644 index 000000000..74c7cc87c --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/BasicDAO.kt @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: 2017-2022 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Update +import io.reactivex.rxjava3.core.Flowable + +@Dao +interface BasicDAO { + + /* Inserts */ + @Insert + fun insert(entity: Entity): Long + + @Insert + fun insertAll(entities: Collection): List + + /* Searches */ + fun getAll(): Flowable> + + fun listByService(serviceId: Int): Flowable> + + /* Deletes */ + @Delete + fun delete(entity: Entity) + + fun deleteAll(): Int + + /* Updates */ + @Update + fun update(entity: Entity): Int + + @Update + fun update(entities: Collection) +} diff --git a/app/src/main/java/org/schabi/newpipe/database/LocalItem.java b/app/src/main/java/org/schabi/newpipe/database/LocalItem.java deleted file mode 100644 index 54b856b06..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/LocalItem.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.schabi.newpipe.database; - -public interface LocalItem { - LocalItemType getLocalItemType(); - - enum LocalItemType { - PLAYLIST_LOCAL_ITEM, - PLAYLIST_REMOTE_ITEM, - - PLAYLIST_STREAM_ITEM, - STATISTIC_STREAM_ITEM, - } -} diff --git a/app/src/main/java/org/schabi/newpipe/database/LocalItem.kt b/app/src/main/java/org/schabi/newpipe/database/LocalItem.kt new file mode 100644 index 000000000..50529610b --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/LocalItem.kt @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2018-2020 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database + +interface LocalItem { + val localItemType: LocalItemType + + enum class LocalItemType { + PLAYLIST_LOCAL_ITEM, + PLAYLIST_REMOTE_ITEM, + + PLAYLIST_STREAM_ITEM, + STATISTIC_STREAM_ITEM, + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/Migrations.java b/app/src/main/java/org/schabi/newpipe/database/Migrations.java deleted file mode 100644 index c9f630869..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/Migrations.java +++ /dev/null @@ -1,307 +0,0 @@ -package org.schabi.newpipe.database; - -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.room.migration.Migration; -import androidx.sqlite.db.SupportSQLiteDatabase; - -import org.schabi.newpipe.MainActivity; - -public final class Migrations { - - ///////////////////////////////////////////////////////////////////////////// - // Test new migrations manually by importing a database from daily usage // - // and checking if the migration works (Use the Database Inspector // - // https://developer.android.com/studio/inspect/database). // - // If you add a migration point it out in the pull request, so that // - // others remember to test it themselves. // - ///////////////////////////////////////////////////////////////////////////// - - 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 int DB_VER_4 = 4; - public static final int DB_VER_5 = 5; - public static final int DB_VER_6 = 6; - public static final int DB_VER_7 = 7; - public static final int DB_VER_8 = 8; - public static final int DB_VER_9 = 9; - - private static final String TAG = Migrations.class.getName(); - public static final boolean DEBUG = MainActivity.DEBUG; - - public static final Migration MIGRATION_1_2 = new Migration(DB_VER_1, DB_VER_2) { - @Override - public void migrate(@NonNull final SupportSQLiteDatabase database) { - if (DEBUG) { - Log.d(TAG, "Start migrating database"); - } - /* - * Unfortunately these queries must be hardcoded due to the possibility of - * schema and names changing at a later date, thus invalidating the older migration - * scripts if they are not hardcoded. - * */ - - // Not much we can do about this, since room doesn't create tables before migration. - // It's either this or blasting the entire database anew. - database.execSQL("CREATE INDEX `index_search_history_search` " - + "ON `search_history` (`search`)"); - database.execSQL("CREATE TABLE IF NOT EXISTS `streams` " - + "(`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)"); - database.execSQL("CREATE UNIQUE INDEX `index_streams_service_id_url` " - + "ON `streams` (`service_id`, `url`)"); - database.execSQL("CREATE TABLE IF NOT EXISTS `stream_history` " - + "(`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 )"); - database.execSQL("CREATE INDEX `index_stream_history_stream_id` " - + "ON `stream_history` (`stream_id`)"); - database.execSQL("CREATE TABLE IF NOT EXISTS `stream_state` " - + "(`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 )"); - database.execSQL("CREATE TABLE IF NOT EXISTS `playlists` " - + "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " - + "`name` TEXT, `thumbnail_url` TEXT)"); - database.execSQL("CREATE INDEX `index_playlists_name` ON `playlists` (`name`)"); - database.execSQL("CREATE TABLE IF NOT EXISTS `playlist_stream_join` " - + "(`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)"); - database.execSQL("CREATE UNIQUE INDEX " - + "`index_playlist_stream_join_playlist_id_join_index` " - + "ON `playlist_stream_join` (`playlist_id`, `join_index`)"); - database.execSQL("CREATE INDEX `index_playlist_stream_join_stream_id` " - + "ON `playlist_stream_join` (`stream_id`)"); - database.execSQL("CREATE TABLE IF NOT EXISTS `remote_playlists` " - + "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " - + "`service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, " - + "`thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)"); - database.execSQL("CREATE INDEX `index_remote_playlists_name` " - + "ON `remote_playlists` (`name`)"); - database.execSQL("CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` " - + "ON `remote_playlists` (`service_id`, `url`)"); - - // Populate streams table with existing entries in watch history - // Latest data first, thus ignoring older entries with the same indices - database.execSQL("INSERT OR IGNORE INTO streams (service_id, url, title, " - + "stream_type, duration, uploader, thumbnail_url) " - - + "SELECT service_id, url, title, 'VIDEO_STREAM', duration, " - + "uploader, thumbnail_url " - - + "FROM watch_history " - + "ORDER BY creation_date DESC"); - - // Once the streams have PKs, join them with the normalized history table - // and populate it with the remaining data from watch history - database.execSQL("INSERT INTO stream_history (stream_id, access_date, repeat_count)" - + "SELECT uid, creation_date, 1 " - + "FROM watch_history INNER JOIN streams " - + "ON watch_history.service_id == streams.service_id " - + "AND watch_history.url == streams.url " - + "ORDER BY creation_date DESC"); - - database.execSQL("DROP TABLE IF EXISTS watch_history"); - - if (DEBUG) { - Log.d(TAG, "Stop migrating database"); - } - } - }; - - public static final Migration MIGRATION_2_3 = new Migration(DB_VER_2, DB_VER_3) { - @Override - public void migrate(@NonNull final 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, " - + "is_upload_date_approximation 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, is_upload_date_approximation) " - - + "SELECT uid, service_id, url, ifnull(title, ''), " - + "ifnull(stream_type, 'VIDEO_STREAM'), ifnull(duration, 0), " - + "ifnull(uploader, ''), ifnull(thumbnail_url, ''), NULL, NULL, NULL, NULL " - - + "FROM streams WHERE url IS NOT NULL"); - - 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, sort_order INTEGER NOT NULL)"); - database.execSQL("CREATE INDEX index_feed_group_sort_order ON feed_group (sort_order)"); - 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)"); - database.execSQL("CREATE TABLE IF NOT EXISTS feed_last_updated " - + "(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)"); - } - }; - - public static final Migration MIGRATION_3_4 = new Migration(DB_VER_3, DB_VER_4) { - @Override - public void migrate(@NonNull final SupportSQLiteDatabase database) { - database.execSQL( - "ALTER TABLE streams ADD COLUMN uploader_url TEXT" - ); - } - }; - - public static final Migration MIGRATION_4_5 = new Migration(DB_VER_4, DB_VER_5) { - @Override - public void migrate(@NonNull final SupportSQLiteDatabase database) { - database.execSQL("ALTER TABLE `subscriptions` ADD COLUMN `notification_mode` " - + "INTEGER NOT NULL DEFAULT 0"); - } - }; - - public static final Migration MIGRATION_5_6 = new Migration(DB_VER_5, DB_VER_6) { - @Override - public void migrate(@NonNull final SupportSQLiteDatabase database) { - database.execSQL("ALTER TABLE `playlists` ADD COLUMN `is_thumbnail_permanent` " - + "INTEGER NOT NULL DEFAULT 0"); - } - }; - - public static final Migration MIGRATION_6_7 = new Migration(DB_VER_6, DB_VER_7) { - @Override - public void migrate(@NonNull final SupportSQLiteDatabase database) { - // Create a new column thumbnail_stream_id - database.execSQL("ALTER TABLE `playlists` ADD COLUMN `thumbnail_stream_id` " - + "INTEGER NOT NULL DEFAULT -1"); - - // Migrate the thumbnail_url to the thumbnail_stream_id - database.execSQL("UPDATE playlists SET thumbnail_stream_id = (" - + " SELECT CASE WHEN COUNT(*) != 0 then stream_uid ELSE -1 END" - + " FROM (" - + " SELECT p.uid AS playlist_uid, s.uid AS stream_uid" - + " FROM playlists p" - + " LEFT JOIN playlist_stream_join ps ON p.uid = ps.playlist_id" - + " LEFT JOIN streams s ON s.uid = ps.stream_id" - + " WHERE s.thumbnail_url = p.thumbnail_url) AS temporary_table" - + " WHERE playlist_uid = playlists.uid)"); - - // Remove the thumbnail_url field in the playlist table - database.execSQL("CREATE TABLE IF NOT EXISTS `playlists_new`" - + "(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " - + "name TEXT, " - + "is_thumbnail_permanent INTEGER NOT NULL, " - + "thumbnail_stream_id INTEGER NOT NULL)"); - - database.execSQL("INSERT INTO playlists_new" - + " SELECT uid, name, is_thumbnail_permanent, thumbnail_stream_id " - + " FROM playlists"); - - - database.execSQL("DROP TABLE playlists"); - database.execSQL("ALTER TABLE playlists_new RENAME TO playlists"); - database.execSQL("CREATE INDEX IF NOT EXISTS " - + "`index_playlists_name` ON `playlists` (`name`)"); - } - }; - - public static final Migration MIGRATION_7_8 = new Migration(DB_VER_7, DB_VER_8) { - @Override - public void migrate(@NonNull final SupportSQLiteDatabase database) { - database.execSQL("DELETE FROM search_history WHERE id NOT IN (SELECT id FROM (SELECT " - + "MIN(id) as id FROM search_history GROUP BY trim(search), service_id ) tmp)"); - database.execSQL("UPDATE search_history SET search = trim(search)"); - } - }; - - public static final Migration MIGRATION_8_9 = new Migration(DB_VER_8, DB_VER_9) { - @Override - public void migrate(@NonNull final SupportSQLiteDatabase database) { - try { - database.beginTransaction(); - - // Update playlists. - // Create a temp table to initialize display_index. - database.execSQL("CREATE TABLE `playlists_tmp` " - + "(`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)"); - database.execSQL("INSERT INTO `playlists_tmp` " - + "(`uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, " - + "`display_index`) " - + "SELECT `uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, " - + "-1 " - + "FROM `playlists`"); - - // Replace the old table, note that this also removes the index on the name which - // we don't need anymore. - database.execSQL("DROP TABLE `playlists`"); - database.execSQL("ALTER TABLE `playlists_tmp` RENAME TO `playlists`"); - - - // Update remote_playlists. - // Create a temp table to initialize display_index. - database.execSQL("CREATE TABLE `remote_playlists_tmp` " - + "(`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)"); - database.execSQL("INSERT INTO `remote_playlists_tmp` (`uid`, `service_id`, " - + "`name`, `url`, `thumbnail_url`, `uploader`, `display_index`, " - + "`stream_count`)" - + "SELECT `uid`, `service_id`, `name`, `url`, `thumbnail_url`, `uploader`, " - + "-1, `stream_count` FROM `remote_playlists`"); - - // Replace the old table, note that this also removes the index on the name which - // we don't need anymore. - database.execSQL("DROP TABLE `remote_playlists`"); - database.execSQL("ALTER TABLE `remote_playlists_tmp` RENAME TO `remote_playlists`"); - - // Create index on the new table. - database.execSQL("CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` " - + "ON `remote_playlists` (`service_id`, `url`)"); - - database.setTransactionSuccessful(); - } finally { - database.endTransaction(); - } - } - }; - - private Migrations() { - } -} diff --git a/app/src/main/java/org/schabi/newpipe/database/Migrations.kt b/app/src/main/java/org/schabi/newpipe/database/Migrations.kt new file mode 100644 index 000000000..8988708e6 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/Migrations.kt @@ -0,0 +1,368 @@ +/* + * SPDX-FileCopyrightText: 2018-2024 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database + +import android.util.Log +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import org.schabi.newpipe.MainActivity + +object Migrations { + + // /////////////////////////////////////////////////////////////////////// // + // Test new migrations manually by importing a database from daily usage // + // and checking if the migration works (Use the Database Inspector // + // https://developer.android.com/studio/inspect/database). // + // If you add a migration point it out in the pull request, so that // + // others remember to test it themselves. // + // /////////////////////////////////////////////////////////////////////// // + + const val DB_VER_1 = 1 + const val DB_VER_2 = 2 + const val DB_VER_3 = 3 + const val DB_VER_4 = 4 + const val DB_VER_5 = 5 + const val DB_VER_6 = 6 + const val DB_VER_7 = 7 + const val DB_VER_8 = 8 + const val DB_VER_9 = 9 + + private val TAG = Migrations::class.java.getName() + private val isDebug = MainActivity.DEBUG + + val MIGRATION_1_2 = object : Migration(DB_VER_1, DB_VER_2) { + override fun migrate(db: SupportSQLiteDatabase) { + if (isDebug) { + Log.d(TAG, "Start migrating database") + } + + /* + * Unfortunately these queries must be hardcoded due to the possibility of + * schema and names changing at a later date, thus invalidating the older migration + * scripts if they are not hardcoded. + * */ + + // Not much we can do about this, since room doesn't create tables before migration. + // It's either this or blasting the entire database anew. + db.execSQL( + "CREATE INDEX `index_search_history_search` " + + "ON `search_history` (`search`)" + ) + db.execSQL( + "CREATE TABLE IF NOT EXISTS `streams` " + + "(`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)" + ) + db.execSQL( + "CREATE UNIQUE INDEX `index_streams_service_id_url` " + + "ON `streams` (`service_id`, `url`)" + ) + db.execSQL( + "CREATE TABLE IF NOT EXISTS `stream_history` " + + "(`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 )" + ) + db.execSQL( + "CREATE INDEX `index_stream_history_stream_id` " + + "ON `stream_history` (`stream_id`)" + ) + db.execSQL( + "CREATE TABLE IF NOT EXISTS `stream_state` " + + "(`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 )" + ) + db.execSQL( + "CREATE TABLE IF NOT EXISTS `playlists` " + + "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "`name` TEXT, `thumbnail_url` TEXT)" + ) + db.execSQL("CREATE INDEX `index_playlists_name` ON `playlists` (`name`)") + db.execSQL( + "CREATE TABLE IF NOT EXISTS `playlist_stream_join` " + + "(`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)" + ) + db.execSQL( + "CREATE UNIQUE INDEX " + + "`index_playlist_stream_join_playlist_id_join_index` " + + "ON `playlist_stream_join` (`playlist_id`, `join_index`)" + ) + db.execSQL( + "CREATE INDEX `index_playlist_stream_join_stream_id` " + + "ON `playlist_stream_join` (`stream_id`)" + ) + db.execSQL( + "CREATE TABLE IF NOT EXISTS `remote_playlists` " + + "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "`service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, " + + "`thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)" + ) + db.execSQL( + "CREATE INDEX `index_remote_playlists_name` " + + "ON `remote_playlists` (`name`)" + ) + db.execSQL( + "CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` " + + "ON `remote_playlists` (`service_id`, `url`)" + ) + + // Populate streams table with existing entries in watch history + // Latest data first, thus ignoring older entries with the same indices + db.execSQL( + "INSERT OR IGNORE INTO streams (service_id, url, title, " + + "stream_type, duration, uploader, thumbnail_url) " + + + "SELECT service_id, url, title, 'VIDEO_STREAM', duration, " + + "uploader, thumbnail_url " + + + "FROM watch_history " + + "ORDER BY creation_date DESC" + ) + + // Once the streams have PKs, join them with the normalized history table + // and populate it with the remaining data from watch history + db.execSQL( + "INSERT INTO stream_history (stream_id, access_date, repeat_count)" + + "SELECT uid, creation_date, 1 " + + "FROM watch_history INNER JOIN streams " + + "ON watch_history.service_id == streams.service_id " + + "AND watch_history.url == streams.url " + + "ORDER BY creation_date DESC" + ) + + db.execSQL("DROP TABLE IF EXISTS watch_history") + + if (isDebug) { + Log.d(TAG, "Stop migrating database") + } + } + } + + val MIGRATION_2_3 = object : Migration(DB_VER_2, DB_VER_3) { + override fun migrate(db: SupportSQLiteDatabase) { + // Add NOT NULLs and new fields + db.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, " + + "is_upload_date_approximation INTEGER)" + ) + + db.execSQL( + "INSERT INTO streams_new (uid, service_id, url, title, stream_type, " + + "duration, uploader, thumbnail_url, view_count, textual_upload_date, " + + "upload_date, is_upload_date_approximation) " + + + "SELECT uid, service_id, url, ifnull(title, ''), " + + "ifnull(stream_type, 'VIDEO_STREAM'), ifnull(duration, 0), " + + "ifnull(uploader, ''), ifnull(thumbnail_url, ''), NULL, NULL, NULL, NULL " + + + "FROM streams WHERE url IS NOT NULL" + ) + + db.execSQL("DROP TABLE streams") + db.execSQL("ALTER TABLE streams_new RENAME TO streams") + db.execSQL( + "CREATE UNIQUE INDEX index_streams_service_id_url " + + "ON streams (service_id, url)" + ) + + // Tables for feed feature + db.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)" + ) + db.execSQL("CREATE INDEX index_feed_subscription_id ON feed (subscription_id)") + db.execSQL( + "CREATE TABLE IF NOT EXISTS feed_group " + + "(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name TEXT NOT NULL, " + + "icon_id INTEGER NOT NULL, sort_order INTEGER NOT NULL)" + ) + db.execSQL("CREATE INDEX index_feed_group_sort_order ON feed_group (sort_order)") + db.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)" + ) + db.execSQL( + "CREATE INDEX index_feed_group_subscription_join_subscription_id " + + "ON feed_group_subscription_join (subscription_id)" + ) + db.execSQL( + "CREATE TABLE IF NOT EXISTS feed_last_updated " + + "(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)" + ) + } + } + + val MIGRATION_3_4 = object : Migration(DB_VER_3, DB_VER_4) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE streams ADD COLUMN uploader_url TEXT") + } + } + + val MIGRATION_4_5 = object : Migration(DB_VER_4, DB_VER_5) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + "ALTER TABLE `subscriptions` ADD COLUMN `notification_mode` " + + "INTEGER NOT NULL DEFAULT 0" + ) + } + } + + val MIGRATION_5_6 = object : Migration(DB_VER_5, DB_VER_6) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + "ALTER TABLE `playlists` ADD COLUMN `is_thumbnail_permanent` " + + "INTEGER NOT NULL DEFAULT 0" + ) + } + } + + val MIGRATION_6_7 = object : Migration(DB_VER_6, DB_VER_7) { + override fun migrate(db: SupportSQLiteDatabase) { + // Create a new column thumbnail_stream_id + db.execSQL( + "ALTER TABLE `playlists` ADD COLUMN `thumbnail_stream_id` " + + "INTEGER NOT NULL DEFAULT -1" + ) + + // Migrate the thumbnail_url to the thumbnail_stream_id + db.execSQL( + "UPDATE playlists SET thumbnail_stream_id = (" + + " SELECT CASE WHEN COUNT(*) != 0 then stream_uid ELSE -1 END" + + " FROM (" + + " SELECT p.uid AS playlist_uid, s.uid AS stream_uid" + + " FROM playlists p" + + " LEFT JOIN playlist_stream_join ps ON p.uid = ps.playlist_id" + + " LEFT JOIN streams s ON s.uid = ps.stream_id" + + " WHERE s.thumbnail_url = p.thumbnail_url) AS temporary_table" + + " WHERE playlist_uid = playlists.uid)" + ) + + // Remove the thumbnail_url field in the playlist table + db.execSQL( + "CREATE TABLE IF NOT EXISTS `playlists_new`" + + "(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "name TEXT, " + + "is_thumbnail_permanent INTEGER NOT NULL, " + + "thumbnail_stream_id INTEGER NOT NULL)" + ) + + db.execSQL( + "INSERT INTO playlists_new" + + " SELECT uid, name, is_thumbnail_permanent, thumbnail_stream_id " + + " FROM playlists" + ) + + db.execSQL("DROP TABLE playlists") + db.execSQL("ALTER TABLE playlists_new RENAME TO playlists") + db.execSQL( + "CREATE INDEX IF NOT EXISTS " + + "`index_playlists_name` ON `playlists` (`name`)" + ) + } + } + + val MIGRATION_7_8 = object : Migration(DB_VER_7, DB_VER_8) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + "DELETE FROM search_history WHERE id NOT IN (SELECT id FROM (SELECT " + + "MIN(id) as id FROM search_history GROUP BY trim(search), service_id ) tmp)" + ) + db.execSQL("UPDATE search_history SET search = trim(search)") + } + } + + val MIGRATION_8_9 = object : Migration(DB_VER_8, DB_VER_9) { + override fun migrate(db: SupportSQLiteDatabase) { + try { + db.beginTransaction() + + // Update playlists. + // Create a temp table to initialize display_index. + db.execSQL( + "CREATE TABLE `playlists_tmp` " + + "(`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)" + ) + db.execSQL( + "INSERT INTO `playlists_tmp` " + + "(`uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, " + + "`display_index`) " + + "SELECT `uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, " + + "-1 " + + "FROM `playlists`" + ) + + // Replace the old table, note that this also removes the index on the name which + // we don't need anymore. + db.execSQL("DROP TABLE `playlists`") + db.execSQL("ALTER TABLE `playlists_tmp` RENAME TO `playlists`") + + // Update remote_playlists. + // Create a temp table to initialize display_index. + db.execSQL( + "CREATE TABLE `remote_playlists_tmp` " + + "(`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)" + ) + db.execSQL( + "INSERT INTO `remote_playlists_tmp` (`uid`, `service_id`, " + + "`name`, `url`, `thumbnail_url`, `uploader`, `display_index`, " + + "`stream_count`)" + + "SELECT `uid`, `service_id`, `name`, `url`, `thumbnail_url`, `uploader`, " + + "-1, `stream_count` FROM `remote_playlists`" + ) + + // Replace the old table, note that this also removes the index on the name which + // we don't need anymore. + db.execSQL("DROP TABLE `remote_playlists`") + db.execSQL("ALTER TABLE `remote_playlists_tmp` RENAME TO `remote_playlists`") + + // Create index on the new table. + db.execSQL( + "CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` " + + "ON `remote_playlists` (`service_id`, `url`)" + ) + + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/HistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/HistoryDAO.java deleted file mode 100644 index 1ade08122..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/history/dao/HistoryDAO.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.schabi.newpipe.database.history.dao; - -import org.schabi.newpipe.database.BasicDAO; - -public interface HistoryDAO extends BasicDAO { - T getLatestEntry(); -} diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/HistoryDAO.kt b/app/src/main/java/org/schabi/newpipe/database/history/dao/HistoryDAO.kt new file mode 100644 index 000000000..d986d0c3b --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/HistoryDAO.kt @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2017 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database.history.dao + +import org.schabi.newpipe.database.BasicDAO + +interface HistoryDAO : BasicDAO { + val latestEntry: T +} diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java deleted file mode 100644 index 8a281bdb4..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java +++ /dev/null @@ -1,52 +0,0 @@ -package org.schabi.newpipe.database.history.dao; - -import androidx.annotation.Nullable; -import androidx.room.Dao; -import androidx.room.Query; - -import org.schabi.newpipe.database.history.model.SearchHistoryEntry; - -import java.util.List; - -import io.reactivex.rxjava3.core.Flowable; - -import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.CREATION_DATE; -import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.ID; -import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.SEARCH; -import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.SERVICE_ID; -import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.TABLE_NAME; - -@Dao -public interface SearchHistoryDAO extends HistoryDAO { - String ORDER_BY_CREATION_DATE = " ORDER BY " + CREATION_DATE + " DESC"; - String ORDER_BY_MAX_CREATION_DATE = " ORDER BY MAX(" + CREATION_DATE + ") DESC"; - - @Query("SELECT * FROM " + TABLE_NAME - + " WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")") - @Nullable - SearchHistoryEntry getLatestEntry(); - - @Query("DELETE FROM " + TABLE_NAME) - @Override - int deleteAll(); - - @Query("DELETE FROM " + TABLE_NAME + " WHERE " + SEARCH + " = :query") - int deleteAllWhereQuery(String query); - - @Query("SELECT * FROM " + TABLE_NAME + ORDER_BY_CREATION_DATE) - @Override - Flowable> getAll(); - - @Query("SELECT " + SEARCH + " FROM " + TABLE_NAME + " GROUP BY " + SEARCH - + ORDER_BY_MAX_CREATION_DATE + " LIMIT :limit") - Flowable> getUniqueEntries(int limit); - - @Query("SELECT * FROM " + TABLE_NAME - + " WHERE " + SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE) - @Override - Flowable> listByService(int serviceId); - - @Query("SELECT " + SEARCH + " FROM " + TABLE_NAME + " WHERE " + SEARCH + " LIKE :query || '%'" - + " GROUP BY " + SEARCH + ORDER_BY_MAX_CREATION_DATE + " LIMIT :limit") - Flowable> getSimilarEntries(String query, int limit); -} diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.kt b/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.kt new file mode 100644 index 000000000..a249721fc --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.kt @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: 2017-2021 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database.history.dao + +import androidx.room.Dao +import androidx.room.Query +import io.reactivex.rxjava3.core.Flowable +import org.schabi.newpipe.database.history.model.SearchHistoryEntry + +@Dao +interface SearchHistoryDAO : HistoryDAO { + + @get:Query("SELECT * FROM search_history WHERE id = (SELECT MAX(id) FROM search_history)") + override val latestEntry: SearchHistoryEntry + + @Query("DELETE FROM search_history") + override fun deleteAll(): Int + + @Query("DELETE FROM search_history WHERE search = :query") + fun deleteAllWhereQuery(query: String): Int + + @Query("SELECT * FROM search_history ORDER BY creation_date DESC") + override fun getAll(): Flowable> + + @Query("SELECT search FROM search_history GROUP BY search ORDER BY MAX(creation_date) DESC LIMIT :limit") + fun getUniqueEntries(limit: Int): Flowable> + + @Query("SELECT * FROM search_history WHERE service_id = :serviceId ORDER BY creation_date DESC") + override fun listByService(serviceId: Int): Flowable> + + @Query( + """ + SELECT search FROM search_history WHERE search LIKE :query || + '%' GROUP BY search ORDER BY MAX(creation_date) DESC LIMIT :limit + """ + ) + fun getSimilarEntries(query: String, limit: Int): Flowable> +} diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java deleted file mode 100644 index 150d4a8e5..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java +++ /dev/null @@ -1,89 +0,0 @@ -package org.schabi.newpipe.database.history.dao; - -import androidx.annotation.Nullable; -import androidx.room.Dao; -import androidx.room.Query; -import androidx.room.RewriteQueriesToDropUnusedColumns; - -import org.schabi.newpipe.database.history.model.StreamHistoryEntity; -import org.schabi.newpipe.database.history.model.StreamHistoryEntry; -import org.schabi.newpipe.database.stream.StreamStatisticsEntry; - -import java.util.List; - -import io.reactivex.rxjava3.core.Flowable; - -import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID; -import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_ACCESS_DATE; -import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; -import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_REPEAT_COUNT; -import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_LATEST_DATE; -import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_WATCH_COUNT; -import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID; -import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; -import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS; -import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS; -import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE; - -@Dao -public abstract class StreamHistoryDAO implements HistoryDAO { - @Query("SELECT * FROM " + STREAM_HISTORY_TABLE - + " WHERE " + STREAM_ACCESS_DATE + " = " - + "(SELECT MAX(" + STREAM_ACCESS_DATE + ") FROM " + STREAM_HISTORY_TABLE + ")") - @Override - @Nullable - public abstract StreamHistoryEntity getLatestEntry(); - - @Override - @Query("SELECT * FROM " + STREAM_HISTORY_TABLE) - public abstract Flowable> getAll(); - - @Override - @Query("DELETE FROM " + STREAM_HISTORY_TABLE) - public abstract int deleteAll(); - - @Override - public Flowable> listByService(final int serviceId) { - throw new UnsupportedOperationException(); - } - - @Query("SELECT * FROM " + STREAM_TABLE - + " INNER JOIN " + STREAM_HISTORY_TABLE - + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID - + " ORDER BY " + STREAM_ACCESS_DATE + " DESC") - public abstract Flowable> getHistory(); - - - @Query("SELECT * FROM " + STREAM_TABLE - + " INNER JOIN " + STREAM_HISTORY_TABLE - + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID - + " ORDER BY " + STREAM_ID + " ASC") - public abstract Flowable> getHistorySortedById(); - - @Query("SELECT * FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID - + " = :streamId ORDER BY " + STREAM_ACCESS_DATE + " DESC LIMIT 1") - @Nullable - public abstract StreamHistoryEntity getLatestEntry(long streamId); - - @Query("DELETE FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId") - public abstract int deleteStreamHistory(long streamId); - - @RewriteQueriesToDropUnusedColumns - @Query("SELECT * FROM " + STREAM_TABLE - - // Select the latest entry and watch count for each stream id on history table - + " INNER JOIN " - + "(SELECT " + JOIN_STREAM_ID + ", " - + " MAX(" + STREAM_ACCESS_DATE + ") AS " + STREAM_LATEST_DATE + ", " - + " SUM(" + STREAM_REPEAT_COUNT + ") AS " + STREAM_WATCH_COUNT - + " FROM " + STREAM_HISTORY_TABLE + " GROUP BY " + JOIN_STREAM_ID + ")" - - + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID - - + " LEFT JOIN " - + "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", " - + STREAM_PROGRESS_MILLIS - + " FROM " + STREAM_STATE_TABLE + " )" - + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS) - public abstract Flowable> getStatistics(); -} diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.kt b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.kt new file mode 100644 index 000000000..dfea41e12 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.kt @@ -0,0 +1,63 @@ +/* + * SPDX-FileCopyrightText: 2018-2022 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database.history.dao + +import androidx.room.Dao +import androidx.room.Query +import androidx.room.RewriteQueriesToDropUnusedColumns +import io.reactivex.rxjava3.core.Flowable +import org.schabi.newpipe.database.history.model.StreamHistoryEntity +import org.schabi.newpipe.database.history.model.StreamHistoryEntry +import org.schabi.newpipe.database.stream.StreamStatisticsEntry + +@Dao +abstract class StreamHistoryDAO : HistoryDAO { + + @get:Query("SELECT * FROM stream_history WHERE access_date = (SELECT MAX(access_date) FROM stream_history)") + abstract override val latestEntry: StreamHistoryEntity + + @Query("SELECT * FROM stream_history") + abstract override fun getAll(): Flowable> + + @Query("DELETE FROM stream_history") + abstract override fun deleteAll(): Int + + override fun listByService(serviceId: Int): Flowable> { + throw UnsupportedOperationException() + } + + @get:Query("SELECT * FROM streams INNER JOIN stream_history ON uid = stream_id ORDER BY access_date DESC") + abstract val history: Flowable> + + @get:Query("SELECT * FROM streams INNER JOIN stream_history ON uid = stream_id ORDER BY uid ASC") + abstract val historySortedById: Flowable> + + @Query("SELECT * FROM stream_history WHERE stream_id = :streamId ORDER BY access_date DESC LIMIT 1") + abstract fun getLatestEntry(streamId: Long): StreamHistoryEntity + + @Query("DELETE FROM stream_history WHERE stream_id = :streamId") + abstract fun deleteStreamHistory(streamId: Long): Int + + // Select the latest entry and watch count for each stream id on history table + @RewriteQueriesToDropUnusedColumns + @Query( + """ + SELECT * FROM streams + + INNER JOIN ( + SELECT stream_id, MAX(access_date) AS latestAccess, SUM(repeat_count) AS watchCount + FROM stream_history + GROUP BY stream_id + ) + ON uid = stream_id + + LEFT JOIN (SELECT stream_id AS stream_id_alias, progress_time FROM stream_state ) + ON uid = stream_id_alias + """ + ) + abstract fun getStatistics(): Flowable> +} diff --git a/app/src/main/java/org/schabi/newpipe/database/history/model/SearchHistoryEntry.kt b/app/src/main/java/org/schabi/newpipe/database/history/model/SearchHistoryEntry.kt index 8cb9a25ca..e6006a069 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/model/SearchHistoryEntry.kt +++ b/app/src/main/java/org/schabi/newpipe/database/history/model/SearchHistoryEntry.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2022 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + package org.schabi.newpipe.database.history.model import androidx.room.ColumnInfo @@ -11,23 +17,24 @@ import java.time.OffsetDateTime tableName = SearchHistoryEntry.TABLE_NAME, indices = [Index(value = [SearchHistoryEntry.SEARCH])] ) -data class SearchHistoryEntry( - @field:ColumnInfo(name = CREATION_DATE) var creationDate: OffsetDateTime?, - @field:ColumnInfo( - name = SERVICE_ID - ) var serviceId: Int, - @field:ColumnInfo(name = SEARCH) var search: String? -) { +data class SearchHistoryEntry @JvmOverloads constructor( + @ColumnInfo(name = CREATION_DATE) + var creationDate: OffsetDateTime?, + + @ColumnInfo(name = SERVICE_ID) + val serviceId: Int, + + @ColumnInfo(name = SEARCH) + val search: String?, + @ColumnInfo(name = ID) @PrimaryKey(autoGenerate = true) - var id: Long = 0 + val id: Long = 0, +) { @Ignore fun hasEqualValues(otherEntry: SearchHistoryEntry): Boolean { - return ( - serviceId == otherEntry.serviceId && - search == otherEntry.search - ) + return serviceId == otherEntry.serviceId && search == otherEntry.search } companion object { diff --git a/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntity.java b/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntity.java deleted file mode 100644 index a9d69afe8..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntity.java +++ /dev/null @@ -1,81 +0,0 @@ -package org.schabi.newpipe.database.history.model; - -import androidx.annotation.NonNull; -import androidx.room.ColumnInfo; -import androidx.room.Entity; -import androidx.room.ForeignKey; -import androidx.room.Index; - -import org.schabi.newpipe.database.stream.model.StreamEntity; - -import java.time.OffsetDateTime; - -import static androidx.room.ForeignKey.CASCADE; -import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID; -import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_ACCESS_DATE; -import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; - -@Entity(tableName = STREAM_HISTORY_TABLE, - primaryKeys = {JOIN_STREAM_ID, STREAM_ACCESS_DATE}, - // No need to index for timestamp as they will almost always be unique - indices = {@Index(value = {JOIN_STREAM_ID})}, - foreignKeys = { - @ForeignKey(entity = StreamEntity.class, - parentColumns = StreamEntity.STREAM_ID, - childColumns = JOIN_STREAM_ID, - onDelete = CASCADE, onUpdate = CASCADE) - }) -public class StreamHistoryEntity { - public static final String STREAM_HISTORY_TABLE = "stream_history"; - public static final String JOIN_STREAM_ID = "stream_id"; - public static final String STREAM_ACCESS_DATE = "access_date"; - public static final String STREAM_REPEAT_COUNT = "repeat_count"; - - @ColumnInfo(name = JOIN_STREAM_ID) - private long streamUid; - - @NonNull - @ColumnInfo(name = STREAM_ACCESS_DATE) - private OffsetDateTime accessDate; - - @ColumnInfo(name = STREAM_REPEAT_COUNT) - private long repeatCount; - - /** - * @param streamUid the stream id this history item will refer to - * @param accessDate the last time the stream was accessed - * @param repeatCount the total number of views this stream received - */ - public StreamHistoryEntity(final long streamUid, - @NonNull final OffsetDateTime accessDate, - final long repeatCount) { - this.streamUid = streamUid; - this.accessDate = accessDate; - this.repeatCount = repeatCount; - } - - public long getStreamUid() { - return streamUid; - } - - public void setStreamUid(final long streamUid) { - this.streamUid = streamUid; - } - - @NonNull - public OffsetDateTime getAccessDate() { - return accessDate; - } - - public void setAccessDate(@NonNull final OffsetDateTime accessDate) { - this.accessDate = accessDate; - } - - public long getRepeatCount() { - return repeatCount; - } - - public void setRepeatCount(final long repeatCount) { - this.repeatCount = repeatCount; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntity.kt b/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntity.kt new file mode 100644 index 000000000..db41e141c --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntity.kt @@ -0,0 +1,56 @@ +/* + * SPDX-FileCopyrightText: 2018-2022 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database.history.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.ForeignKey.Companion.CASCADE +import androidx.room.Index +import org.schabi.newpipe.database.history.model.StreamHistoryEntity.Companion.JOIN_STREAM_ID +import org.schabi.newpipe.database.history.model.StreamHistoryEntity.Companion.STREAM_ACCESS_DATE +import org.schabi.newpipe.database.history.model.StreamHistoryEntity.Companion.STREAM_HISTORY_TABLE +import org.schabi.newpipe.database.stream.model.StreamEntity +import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID +import java.time.OffsetDateTime + +/** + * @param streamUid the stream id this history item will refer to + * @param accessDate the last time the stream was accessed + * @param repeatCount the total number of views this stream received + */ +@Entity( + tableName = STREAM_HISTORY_TABLE, + primaryKeys = [JOIN_STREAM_ID, STREAM_ACCESS_DATE], + indices = [Index(value = [JOIN_STREAM_ID])], + foreignKeys = [ + ForeignKey( + entity = StreamEntity::class, + parentColumns = arrayOf(STREAM_ID), + childColumns = arrayOf(JOIN_STREAM_ID), + onDelete = CASCADE, + onUpdate = CASCADE + ) + ] +) +data class StreamHistoryEntity( + @ColumnInfo(name = JOIN_STREAM_ID) + val streamUid: Long, + + @ColumnInfo(name = STREAM_ACCESS_DATE) + var accessDate: OffsetDateTime, + + @ColumnInfo(name = STREAM_REPEAT_COUNT) + var repeatCount: Long +) { + companion object { + const val STREAM_HISTORY_TABLE: String = "stream_history" + const val STREAM_ACCESS_DATE: String = "access_date" + const val JOIN_STREAM_ID: String = "stream_id" + const val STREAM_REPEAT_COUNT: String = "repeat_count" + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistDuplicatesEntry.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistDuplicatesEntry.java deleted file mode 100644 index 3be85e6e1..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistDuplicatesEntry.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.schabi.newpipe.database.playlist; - -import androidx.room.ColumnInfo; - -/** - * This class adds a field to {@link PlaylistMetadataEntry} that contains an integer representing - * how many times a specific stream is already contained inside a local playlist. Used to be able - * to grey out playlists which already contain the current stream in the playlist append dialog. - * @see org.schabi.newpipe.local.playlist.LocalPlaylistManager#getPlaylistDuplicates(String) - */ -public class PlaylistDuplicatesEntry extends PlaylistMetadataEntry { - public static final String PLAYLIST_TIMES_STREAM_IS_CONTAINED = "timesStreamIsContained"; - @ColumnInfo(name = PLAYLIST_TIMES_STREAM_IS_CONTAINED) - public final long timesStreamIsContained; - - @SuppressWarnings("checkstyle:ParameterNumber") - public PlaylistDuplicatesEntry(final long uid, - final String name, - final String thumbnailUrl, - final boolean isThumbnailPermanent, - final long thumbnailStreamId, - final long displayIndex, - final long streamCount, - final long timesStreamIsContained) { - super(uid, name, thumbnailUrl, isThumbnailPermanent, thumbnailStreamId, displayIndex, - streamCount); - this.timesStreamIsContained = timesStreamIsContained; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistDuplicatesEntry.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistDuplicatesEntry.kt new file mode 100644 index 000000000..f0447df6e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistDuplicatesEntry.kt @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: 2023-2024 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database.playlist + +import androidx.room.ColumnInfo + +/** + * This class adds a field to [PlaylistMetadataEntry] that contains an integer representing + * how many times a specific stream is already contained inside a local playlist. Used to be able + * to grey out playlists which already contain the current stream in the playlist append dialog. + * @see org.schabi.newpipe.local.playlist.LocalPlaylistManager.getPlaylistDuplicates + */ +data class PlaylistDuplicatesEntry( + override val uid: Long, + override val thumbnailUrl: String?, + override val isThumbnailPermanent: Boolean?, + override val thumbnailStreamId: Long?, + override var displayIndex: Long?, + override val streamCount: Long, + override val orderingName: String?, + + @ColumnInfo(name = PLAYLIST_TIMES_STREAM_IS_CONTAINED) + val timesStreamIsContained: Long +) : PlaylistMetadataEntry( + uid = uid, + orderingName = orderingName, + thumbnailUrl = thumbnailUrl, + isThumbnailPermanent = isThumbnailPermanent, + thumbnailStreamId = thumbnailStreamId, + displayIndex = displayIndex, + streamCount = streamCount +) { + companion object { + const val PLAYLIST_TIMES_STREAM_IS_CONTAINED: String = "timesStreamIsContained" + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java deleted file mode 100644 index 91f4622e9..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.schabi.newpipe.database.playlist; - -import androidx.annotation.Nullable; - -import org.schabi.newpipe.database.LocalItem; - -public interface PlaylistLocalItem extends LocalItem { - String getOrderingName(); - - long getDisplayIndex(); - - long getUid(); - - void setDisplayIndex(long displayIndex); - - @Nullable - String getThumbnailUrl(); -} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.kt new file mode 100644 index 000000000..4f2f79aa0 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.kt @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2018-2025 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database.playlist + +import org.schabi.newpipe.database.LocalItem + +interface PlaylistLocalItem : LocalItem { + val orderingName: String? + val displayIndex: Long? + val uid: Long + val thumbnailUrl: String? +} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java deleted file mode 100644 index 8fbadb020..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java +++ /dev/null @@ -1,82 +0,0 @@ -package org.schabi.newpipe.database.playlist; - -import androidx.room.ColumnInfo; - -import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_DISPLAY_INDEX; -import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID; -import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME; -import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_PERMANENT; -import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_STREAM_ID; -import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL; - -import androidx.annotation.Nullable; - -public class PlaylistMetadataEntry implements PlaylistLocalItem { - public static final String PLAYLIST_STREAM_COUNT = "streamCount"; - - @ColumnInfo(name = PLAYLIST_ID) - private final long uid; - @ColumnInfo(name = PLAYLIST_NAME) - public final String name; - @ColumnInfo(name = PLAYLIST_THUMBNAIL_PERMANENT) - private final boolean isThumbnailPermanent; - @ColumnInfo(name = PLAYLIST_THUMBNAIL_STREAM_ID) - private final long thumbnailStreamId; - @ColumnInfo(name = PLAYLIST_THUMBNAIL_URL) - public final String thumbnailUrl; - @ColumnInfo(name = PLAYLIST_DISPLAY_INDEX) - private long displayIndex; - @ColumnInfo(name = PLAYLIST_STREAM_COUNT) - public final long streamCount; - - public PlaylistMetadataEntry(final long uid, final String name, final String thumbnailUrl, - final boolean isThumbnailPermanent, final long thumbnailStreamId, - final long displayIndex, final long streamCount) { - this.uid = uid; - this.name = name; - this.thumbnailUrl = thumbnailUrl; - this.isThumbnailPermanent = isThumbnailPermanent; - this.thumbnailStreamId = thumbnailStreamId; - this.displayIndex = displayIndex; - this.streamCount = streamCount; - } - - @Override - public LocalItemType getLocalItemType() { - return LocalItemType.PLAYLIST_LOCAL_ITEM; - } - - @Override - public String getOrderingName() { - return name; - } - - public boolean isThumbnailPermanent() { - return isThumbnailPermanent; - } - - public long getThumbnailStreamId() { - return thumbnailStreamId; - } - - @Override - public long getDisplayIndex() { - return displayIndex; - } - - @Override - public long getUid() { - return uid; - } - - @Override - public void setDisplayIndex(final long displayIndex) { - this.displayIndex = displayIndex; - } - - @Nullable - @Override - public String getThumbnailUrl() { - return thumbnailUrl; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.kt new file mode 100644 index 000000000..9b62c1380 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.kt @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: 2018-2025 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database.playlist + +import androidx.room.ColumnInfo +import org.schabi.newpipe.database.LocalItem.LocalItemType +import org.schabi.newpipe.database.playlist.model.PlaylistEntity + +open class PlaylistMetadataEntry( + @ColumnInfo(name = PlaylistEntity.PLAYLIST_ID) + override val uid: Long, + + @ColumnInfo(name = PlaylistEntity.PLAYLIST_NAME) + override val orderingName: String?, + + @ColumnInfo(name = PlaylistEntity.PLAYLIST_THUMBNAIL_URL) + override val thumbnailUrl: String?, + + @ColumnInfo(name = PlaylistEntity.PLAYLIST_DISPLAY_INDEX) + override var displayIndex: Long?, + + @ColumnInfo(name = PlaylistEntity.PLAYLIST_THUMBNAIL_PERMANENT) + open val isThumbnailPermanent: Boolean?, + + @ColumnInfo(name = PlaylistEntity.PLAYLIST_THUMBNAIL_STREAM_ID) + open val thumbnailStreamId: Long?, + + @ColumnInfo(name = PLAYLIST_STREAM_COUNT) + open val streamCount: Long +) : PlaylistLocalItem { + + override val localItemType: LocalItemType + get() = LocalItemType.PLAYLIST_LOCAL_ITEM + + companion object { + const val PLAYLIST_STREAM_COUNT: String = "streamCount" + } +} 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 index 1d74c6d31..90fdee2d3 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.kt +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2020-2023 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + package org.schabi.newpipe.database.playlist import androidx.room.ColumnInfo @@ -23,18 +29,21 @@ data class PlaylistStreamEntry( val joinIndex: Int ) : LocalItem { + override val localItemType: LocalItem.LocalItemType + get() = LocalItem.LocalItemType.PLAYLIST_STREAM_ITEM + @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.uploaderUrl = streamEntity.uploaderUrl - item.thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl) - - return item - } - - override fun getLocalItemType(): LocalItem.LocalItemType { - return LocalItem.LocalItemType.PLAYLIST_STREAM_ITEM + return StreamInfoItem( + streamEntity.serviceId, + streamEntity.url, + streamEntity.title, + streamEntity.streamType + ).apply { + duration = streamEntity.duration + uploaderName = streamEntity.uploader + uploaderUrl = streamEntity.uploaderUrl + thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl) + } } } diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.java b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.java deleted file mode 100644 index d8071e0af..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.schabi.newpipe.database.playlist.dao; - -import androidx.room.Dao; -import androidx.room.Query; -import androidx.room.Transaction; - -import org.schabi.newpipe.database.BasicDAO; -import org.schabi.newpipe.database.playlist.model.PlaylistEntity; - -import java.util.List; - -import io.reactivex.rxjava3.core.Flowable; - -import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID; -import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE; - -@Dao -public interface PlaylistDAO extends BasicDAO { - @Override - @Query("SELECT * FROM " + PLAYLIST_TABLE) - Flowable> getAll(); - - @Override - @Query("DELETE FROM " + PLAYLIST_TABLE) - int deleteAll(); - - @Override - default Flowable> listByService(final int serviceId) { - throw new UnsupportedOperationException(); - } - - @Query("SELECT * FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId") - Flowable> getPlaylist(long playlistId); - - @Query("DELETE FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId") - int deletePlaylist(long playlistId); - - @Query("SELECT COUNT(*) FROM " + PLAYLIST_TABLE) - Flowable getCount(); - - @Transaction - default long upsertPlaylist(final PlaylistEntity playlist) { - final long playlistId = playlist.getUid(); - - if (playlistId == -1) { - // This situation is probably impossible. - return insert(playlist); - } else { - update(playlist); - return playlistId; - } - } -} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.kt new file mode 100644 index 000000000..9c2dd89a8 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.kt @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: 2018-2022 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database.playlist.dao + +import androidx.room.Dao +import androidx.room.Query +import androidx.room.Transaction +import io.reactivex.rxjava3.core.Flowable +import org.schabi.newpipe.database.BasicDAO +import org.schabi.newpipe.database.playlist.model.PlaylistEntity + +@Dao +interface PlaylistDAO : BasicDAO { + + @Query("SELECT * FROM playlists") + override fun getAll(): Flowable> + + @Query("DELETE FROM playlists") + override fun deleteAll(): Int + + override fun listByService(serviceId: Int): Flowable> { + throw UnsupportedOperationException() + } + + @Query("SELECT * FROM playlists WHERE uid = :playlistId") + fun getPlaylist(playlistId: Long): Flowable> + + @Query("DELETE FROM playlists WHERE uid = :playlistId") + fun deletePlaylist(playlistId: Long): Int + + @get:Query("SELECT COUNT(*) FROM playlists") + val count: Flowable + + @Transaction + fun upsertPlaylist(playlist: PlaylistEntity): Long { + if (playlist.uid == -1L) { + // This situation is probably impossible. + return insert(playlist) + } else { + update(playlist) + return playlist.uid + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.java b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.java deleted file mode 100644 index ef77d5ade..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.java +++ /dev/null @@ -1,68 +0,0 @@ -package org.schabi.newpipe.database.playlist.dao; - -import androidx.room.Dao; -import androidx.room.Query; -import androidx.room.Transaction; - -import org.schabi.newpipe.database.BasicDAO; -import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; - -import java.util.List; - -import io.reactivex.rxjava3.core.Flowable; - -import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_DISPLAY_INDEX; -import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_ID; -import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_SERVICE_ID; -import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_TABLE; -import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_URL; - -@Dao -public interface PlaylistRemoteDAO extends BasicDAO { - @Override - @Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE) - Flowable> getAll(); - - @Override - @Query("DELETE FROM " + REMOTE_PLAYLIST_TABLE) - int deleteAll(); - - @Override - @Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE - + " WHERE " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId") - Flowable> listByService(int serviceId); - - @Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE " - + REMOTE_PLAYLIST_ID + " = :playlistId") - Flowable getPlaylist(long playlistId); - - @Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE " - + REMOTE_PLAYLIST_URL + " = :url AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId") - Flowable> getPlaylist(long serviceId, String url); - - @Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE - + " ORDER BY " + REMOTE_PLAYLIST_DISPLAY_INDEX) - Flowable> getPlaylists(); - - @Query("SELECT " + REMOTE_PLAYLIST_ID + " FROM " + REMOTE_PLAYLIST_TABLE - + " WHERE " + REMOTE_PLAYLIST_URL + " = :url " - + "AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId") - Long getPlaylistIdInternal(long serviceId, String url); - - @Transaction - default long upsert(final PlaylistRemoteEntity playlist) { - final Long playlistId = getPlaylistIdInternal(playlist.getServiceId(), playlist.getUrl()); - - if (playlistId == null) { - return insert(playlist); - } else { - playlist.setUid(playlistId); - update(playlist); - return playlistId; - } - } - - @Query("DELETE FROM " + REMOTE_PLAYLIST_TABLE - + " WHERE " + REMOTE_PLAYLIST_ID + " = :playlistId") - int deletePlaylist(long playlistId); -} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.kt new file mode 100644 index 000000000..8e0b80c3b --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.kt @@ -0,0 +1,55 @@ +/* + * SPDX-FileCopyrightText: 2018-2025 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database.playlist.dao + +import androidx.room.Dao +import androidx.room.Query +import androidx.room.Transaction +import io.reactivex.rxjava3.core.Flowable +import org.schabi.newpipe.database.BasicDAO +import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity + +@Dao +interface PlaylistRemoteDAO : BasicDAO { + + @Query("SELECT * FROM remote_playlists") + override fun getAll(): Flowable> + + @Query("DELETE FROM remote_playlists") + override fun deleteAll(): Int + + @Query("SELECT * FROM remote_playlists WHERE service_id = :serviceId") + override fun listByService(serviceId: Int): Flowable> + + @Query("SELECT * FROM remote_playlists WHERE uid = :playlistId") + fun getPlaylist(playlistId: Long): Flowable + + @Query("SELECT * FROM remote_playlists WHERE url = :url AND uid = :serviceId") + fun getPlaylist(serviceId: Long, url: String?): Flowable> + + @get:Query("SELECT * FROM remote_playlists ORDER BY display_index") + val playlists: Flowable> + + @Query("SELECT uid FROM remote_playlists WHERE url = :url AND service_id = :serviceId") + fun getPlaylistIdInternal(serviceId: Long, url: String?): Long? + + @Transaction + fun upsert(playlist: PlaylistRemoteEntity): Long { + val playlistId = getPlaylistIdInternal(playlist.serviceId.toLong(), playlist.url) + + if (playlistId == null) { + return insert(playlist) + } else { + playlist.uid = playlistId + update(playlist) + return playlistId + } + } + + @Query("DELETE FROM remote_playlists WHERE uid = :playlistId") + fun deletePlaylist(playlistId: Long): Int +} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java deleted file mode 100644 index 6b77166ea..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java +++ /dev/null @@ -1,159 +0,0 @@ -package org.schabi.newpipe.database.playlist.dao; - -import androidx.room.Dao; -import androidx.room.Query; -import androidx.room.RewriteQueriesToDropUnusedColumns; -import androidx.room.Transaction; - -import org.schabi.newpipe.database.BasicDAO; -import org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry; -import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; -import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; -import org.schabi.newpipe.database.playlist.model.PlaylistEntity; -import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; - -import java.util.List; - -import io.reactivex.rxjava3.core.Flowable; - -import static org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry.PLAYLIST_TIMES_STREAM_IS_CONTAINED; -import static org.schabi.newpipe.database.playlist.PlaylistMetadataEntry.PLAYLIST_STREAM_COUNT; -import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_DISPLAY_INDEX; -import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.DEFAULT_THUMBNAIL; -import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID; -import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME; -import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE; -import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_PERMANENT; -import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_STREAM_ID; -import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL; -import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_INDEX; -import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_PLAYLIST_ID; -import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_STREAM_ID; -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_TABLE; -import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_THUMBNAIL_URL; -import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_URL; -import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS; -import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS; -import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE; - -@Dao -public interface PlaylistStreamDAO extends BasicDAO { - @Override - @Query("SELECT * FROM " + PLAYLIST_STREAM_JOIN_TABLE) - Flowable> getAll(); - - @Override - @Query("DELETE FROM " + PLAYLIST_STREAM_JOIN_TABLE) - int deleteAll(); - - @Override - default Flowable> listByService(final int serviceId) { - throw new UnsupportedOperationException(); - } - - @Query("DELETE FROM " + PLAYLIST_STREAM_JOIN_TABLE - + " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId") - void deleteBatch(long playlistId); - - @Query("SELECT COALESCE(MAX(" + JOIN_INDEX + "), -1)" - + " FROM " + PLAYLIST_STREAM_JOIN_TABLE - + " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId") - Flowable getMaximumIndexOf(long playlistId); - - @Query("SELECT CASE WHEN COUNT(*) != 0 then " + STREAM_ID - + " ELSE " + PlaylistEntity.DEFAULT_THUMBNAIL_ID + " END" - + " FROM " + STREAM_TABLE - + " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE - + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID - + " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId " - + " LIMIT 1" - ) - Flowable getAutomaticThumbnailStreamId(long playlistId); - - @RewriteQueriesToDropUnusedColumns - @Transaction - @Query("SELECT * FROM " + STREAM_TABLE + " INNER JOIN " - // get ids of streams of the given playlist - + "(SELECT " + JOIN_STREAM_ID + "," + JOIN_INDEX - + " FROM " + PLAYLIST_STREAM_JOIN_TABLE - + " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId)" - - // then merge with the stream metadata - + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID - - + " LEFT JOIN " - + "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", " - + STREAM_PROGRESS_MILLIS - + " FROM " + STREAM_STATE_TABLE + " )" - + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS - - + " ORDER BY " + JOIN_INDEX + " ASC") - Flowable> getOrderedStreamsOf(long playlistId); - - @Transaction - @Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", " - + PLAYLIST_THUMBNAIL_PERMANENT + ", " + PLAYLIST_THUMBNAIL_STREAM_ID + ", " - + PLAYLIST_DISPLAY_INDEX + ", " - - + " CASE WHEN " + PLAYLIST_THUMBNAIL_STREAM_ID + " = " - + PlaylistEntity.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + DEFAULT_THUMBNAIL + "'" - + " ELSE (SELECT " + STREAM_THUMBNAIL_URL - + " FROM " + STREAM_TABLE - + " WHERE " + STREAM_TABLE + "." + STREAM_ID + " = " + PLAYLIST_THUMBNAIL_STREAM_ID - + " ) END AS " + PLAYLIST_THUMBNAIL_URL + ", " - - + "COALESCE(COUNT(" + JOIN_PLAYLIST_ID + "), 0) AS " + PLAYLIST_STREAM_COUNT - + " FROM " + PLAYLIST_TABLE - + " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE - + " ON " + PLAYLIST_TABLE + "." + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID - + " GROUP BY " + PLAYLIST_ID - + " ORDER BY " + PLAYLIST_DISPLAY_INDEX) - Flowable> getPlaylistMetadata(); - - @RewriteQueriesToDropUnusedColumns - @Transaction - @Query("SELECT *, MIN(" + JOIN_INDEX + ")" - + " FROM " + STREAM_TABLE + " INNER JOIN" - + " (SELECT " + JOIN_STREAM_ID + "," + JOIN_INDEX - + " FROM " + PLAYLIST_STREAM_JOIN_TABLE - + " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId)" - + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID - + " LEFT JOIN " - + "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", " - + STREAM_PROGRESS_MILLIS - + " FROM " + STREAM_STATE_TABLE + " )" - + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS - + " GROUP BY " + STREAM_ID - + " ORDER BY MIN(" + JOIN_INDEX + ") ASC") - Flowable> getStreamsWithoutDuplicates(long playlistId); - - @Transaction - @Query("SELECT " + PLAYLIST_TABLE + "." + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", " - + PLAYLIST_THUMBNAIL_PERMANENT + ", " + PLAYLIST_THUMBNAIL_STREAM_ID + ", " - + PLAYLIST_DISPLAY_INDEX + ", " - - + " CASE WHEN " + PLAYLIST_THUMBNAIL_STREAM_ID + " = " - + PlaylistEntity.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + DEFAULT_THUMBNAIL + "'" - + " ELSE (SELECT " + STREAM_THUMBNAIL_URL - + " FROM " + STREAM_TABLE - + " WHERE " + STREAM_TABLE + "." + STREAM_ID + " = " + PLAYLIST_THUMBNAIL_STREAM_ID - + " ) END AS " + PLAYLIST_THUMBNAIL_URL + ", " - - + "COALESCE(COUNT(" + JOIN_PLAYLIST_ID + "), 0) AS " + PLAYLIST_STREAM_COUNT + ", " - + "COALESCE(SUM(" + STREAM_URL + " = :streamUrl), 0) AS " - + PLAYLIST_TIMES_STREAM_IS_CONTAINED - - + " FROM " + PLAYLIST_TABLE - + " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE - + " ON " + PLAYLIST_TABLE + "." + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID - - + " LEFT JOIN " + STREAM_TABLE - + " ON " + STREAM_TABLE + "." + STREAM_ID + " = " + JOIN_STREAM_ID - + " AND :streamUrl = :streamUrl" - - + " GROUP BY " + JOIN_PLAYLIST_ID - + " ORDER BY " + PLAYLIST_DISPLAY_INDEX + ", " + PLAYLIST_NAME) - Flowable> getPlaylistDuplicatesMetadata(String streamUrl); -} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.kt new file mode 100644 index 000000000..11ad38685 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.kt @@ -0,0 +1,151 @@ +/* + * SPDX-FileCopyrightText: 2018-2024 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database.playlist.dao + +import androidx.room.Dao +import androidx.room.Query +import androidx.room.RewriteQueriesToDropUnusedColumns +import androidx.room.Transaction +import io.reactivex.rxjava3.core.Flowable +import org.schabi.newpipe.database.BasicDAO +import org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry +import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry +import org.schabi.newpipe.database.playlist.PlaylistStreamEntry +import org.schabi.newpipe.database.playlist.model.PlaylistEntity.Companion.DEFAULT_THUMBNAIL +import org.schabi.newpipe.database.playlist.model.PlaylistEntity.Companion.DEFAULT_THUMBNAIL_ID +import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity + +@Dao +interface PlaylistStreamDAO : BasicDAO { + + @Query("SELECT * FROM playlist_stream_join") + override fun getAll(): Flowable> + + @Query("DELETE FROM playlist_stream_join") + override fun deleteAll(): Int + + override fun listByService(serviceId: Int): Flowable> { + throw UnsupportedOperationException() + } + + @Query("DELETE FROM playlist_stream_join WHERE playlist_id = :playlistId") + fun deleteBatch(playlistId: Long) + + @Query("SELECT COALESCE(MAX(join_index), -1) FROM playlist_stream_join WHERE playlist_id = :playlistId") + fun getMaximumIndexOf(playlistId: Long): Flowable + + @Query( + """ + SELECT CASE WHEN COUNT(*) != 0 then stream_id ELSE $DEFAULT_THUMBNAIL_ID END + FROM streams + + LEFT JOIN playlist_stream_join + ON uid = stream_id + + WHERE playlist_id = :playlistId LIMIT 1 + """ + ) + fun getAutomaticThumbnailStreamId(playlistId: Long): Flowable + + // get ids of streams of the given playlist then merge with the stream metadata + @RewriteQueriesToDropUnusedColumns + @Transaction + @Query( + """ + SELECT * FROM streams + + INNER JOIN (SELECT stream_id, join_index FROM playlist_stream_join WHERE playlist_id = :playlistId) + ON uid = stream_id + + LEFT JOIN (SELECT stream_id AS stream_id_alias, progress_time FROM stream_state ) + ON uid = stream_id_alias + + ORDER BY join_index ASC + """ + ) + fun getOrderedStreamsOf(playlistId: Long): Flowable> + + @Transaction + @Query( + """ + SELECT uid, name, is_thumbnail_permanent, thumbnail_stream_id, display_index, + + CASE WHEN thumbnail_stream_id = $DEFAULT_THUMBNAIL_ID + THEN :defaultThumbnail + ELSE (SELECT thumbnail_url FROM streams WHERE streams.uid = thumbnail_stream_id) + END AS thumbnail_url, + + COALESCE(COUNT(playlist_id), 0) AS streamCount FROM playlists + + LEFT JOIN playlist_stream_join + ON playlists.uid = playlist_id + + GROUP BY uid + ORDER BY display_index + """ + ) + fun getPlaylistMetadata(defaultThumbnail: String): Flowable> + + // TODO: Remove on migrating classes to Kotlin + fun getPlaylistMetadata(): Flowable> { + return getPlaylistMetadata(DEFAULT_THUMBNAIL) + } + + @RewriteQueriesToDropUnusedColumns + @Transaction + @Query( + """ + SELECT *, MIN(join_index) FROM streams + + INNER JOIN (SELECT stream_id, join_index FROM playlist_stream_join WHERE playlist_id = :playlistId) + ON uid = stream_id + + LEFT JOIN (SELECT stream_id AS stream_id_alias, progress_time FROM stream_state ) + ON uid = stream_id_alias + + GROUP BY uid + ORDER BY MIN(join_index) ASC + """ + ) + fun getStreamsWithoutDuplicates(playlistId: Long): Flowable> + + @RewriteQueriesToDropUnusedColumns + @Transaction + @Query( + """ + SELECT playlists.uid, name, is_thumbnail_permanent, thumbnail_stream_id, display_index, + + CASE WHEN thumbnail_stream_id = $DEFAULT_THUMBNAIL_ID + THEN :defaultThumbnail + ELSE (SELECT thumbnail_url FROM streams WHERE streams.uid = thumbnail_stream_id ) + END AS thumbnail_url, + + COALESCE(COUNT(playlist_id), 0) AS streamCount, + COALESCE(SUM(url = :streamUrl), 0) AS timesStreamIsContained FROM playlists + + LEFT JOIN playlist_stream_join + ON playlists.uid = playlist_id + + LEFT JOIN streams + ON streams.uid = stream_id AND :streamUrl = :streamUrl + + GROUP BY playlist_id + ORDER BY display_index, name + """ + ) + fun getPlaylistDuplicatesMetadata( + streamUrl: String, + defaultThumbnail: String + ): Flowable> + + // TODO: Remove on migrating classes to Kotlin + fun getPlaylistDuplicatesMetadata( + streamUrl: String + ): Flowable> { + return getPlaylistDuplicatesMetadata(streamUrl, DEFAULT_THUMBNAIL) + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java deleted file mode 100644 index e0c1a06b7..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java +++ /dev/null @@ -1,100 +0,0 @@ -package org.schabi.newpipe.database.playlist.model; - -import androidx.room.ColumnInfo; -import androidx.room.Entity; -import androidx.room.Ignore; -import androidx.room.PrimaryKey; - -import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; - -@Entity(tableName = PLAYLIST_TABLE) -public class PlaylistEntity { - - public static final String DEFAULT_THUMBNAIL = "drawable://" - + R.drawable.placeholder_thumbnail_playlist; - public static final long DEFAULT_THUMBNAIL_ID = -1; - - public static final String PLAYLIST_TABLE = "playlists"; - public static final String PLAYLIST_ID = "uid"; - public static final String PLAYLIST_NAME = "name"; - public static final String PLAYLIST_THUMBNAIL_URL = "thumbnail_url"; - public static final String PLAYLIST_DISPLAY_INDEX = "display_index"; - public static final String PLAYLIST_THUMBNAIL_PERMANENT = "is_thumbnail_permanent"; - public static final String PLAYLIST_THUMBNAIL_STREAM_ID = "thumbnail_stream_id"; - - @PrimaryKey(autoGenerate = true) - @ColumnInfo(name = PLAYLIST_ID) - private long uid = 0; - - @ColumnInfo(name = PLAYLIST_NAME) - private String name; - - @ColumnInfo(name = PLAYLIST_THUMBNAIL_PERMANENT) - private boolean isThumbnailPermanent; - - @ColumnInfo(name = PLAYLIST_THUMBNAIL_STREAM_ID) - private long thumbnailStreamId; - - @ColumnInfo(name = PLAYLIST_DISPLAY_INDEX) - private long displayIndex; - - public PlaylistEntity(final String name, final boolean isThumbnailPermanent, - final long thumbnailStreamId, final long displayIndex) { - this.name = name; - this.isThumbnailPermanent = isThumbnailPermanent; - this.thumbnailStreamId = thumbnailStreamId; - this.displayIndex = displayIndex; - } - - @Ignore - public PlaylistEntity(final PlaylistMetadataEntry item) { - this.uid = item.getUid(); - this.name = item.name; - this.isThumbnailPermanent = item.isThumbnailPermanent(); - this.thumbnailStreamId = item.getThumbnailStreamId(); - this.displayIndex = item.getDisplayIndex(); - } - - public long getUid() { - return uid; - } - - public void setUid(final long uid) { - this.uid = uid; - } - - public String getName() { - return name; - } - - public void setName(final String name) { - this.name = name; - } - - public long getThumbnailStreamId() { - return thumbnailStreamId; - } - - public void setThumbnailStreamId(final long thumbnailStreamId) { - this.thumbnailStreamId = thumbnailStreamId; - } - - public boolean getIsThumbnailPermanent() { - return isThumbnailPermanent; - } - - public void setIsThumbnailPermanent(final boolean isThumbnailSet) { - this.isThumbnailPermanent = isThumbnailSet; - } - - public long getDisplayIndex() { - return displayIndex; - } - - public void setDisplayIndex(final long displayIndex) { - this.displayIndex = displayIndex; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.kt new file mode 100644 index 000000000..a72a1cee5 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.kt @@ -0,0 +1,58 @@ +/* + * SPDX-FileCopyrightText: 2018-2024 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database.playlist.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Ignore +import androidx.room.PrimaryKey +import org.schabi.newpipe.R +import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry + +@Entity(tableName = PlaylistEntity.Companion.PLAYLIST_TABLE) +data class PlaylistEntity @JvmOverloads constructor( + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = PLAYLIST_ID) + var uid: Long = 0, + + @ColumnInfo(name = PLAYLIST_NAME) + var name: String?, + + @ColumnInfo(name = PLAYLIST_THUMBNAIL_PERMANENT) + var isThumbnailPermanent: Boolean, + + @ColumnInfo(name = PLAYLIST_THUMBNAIL_STREAM_ID) + var thumbnailStreamId: Long, + + @ColumnInfo(name = PLAYLIST_DISPLAY_INDEX) + var displayIndex: Long +) { + + @Ignore + constructor(item: PlaylistMetadataEntry) : this( + uid = item.uid, + name = item.orderingName, + isThumbnailPermanent = item.isThumbnailPermanent!!, + thumbnailStreamId = item.thumbnailStreamId!!, + displayIndex = item.displayIndex!!, + ) + + companion object { + @JvmField + val DEFAULT_THUMBNAIL = "drawable://" + R.drawable.placeholder_thumbnail_playlist + + const val DEFAULT_THUMBNAIL_ID = -1L + + const val PLAYLIST_TABLE = "playlists" + const val PLAYLIST_ID = "uid" + const val PLAYLIST_NAME = "name" + const val PLAYLIST_THUMBNAIL_URL = "thumbnail_url" + const val PLAYLIST_DISPLAY_INDEX = "display_index" + const val PLAYLIST_THUMBNAIL_PERMANENT = "is_thumbnail_permanent" + const val PLAYLIST_THUMBNAIL_STREAM_ID = "thumbnail_stream_id" + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java deleted file mode 100644 index 0b0e3605e..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java +++ /dev/null @@ -1,191 +0,0 @@ -package org.schabi.newpipe.database.playlist.model; - -import android.text.TextUtils; - -import androidx.annotation.Nullable; -import androidx.room.ColumnInfo; -import androidx.room.Entity; -import androidx.room.Ignore; -import androidx.room.Index; -import androidx.room.PrimaryKey; - -import org.schabi.newpipe.database.playlist.PlaylistLocalItem; -import org.schabi.newpipe.extractor.playlist.PlaylistInfo; -import org.schabi.newpipe.util.Constants; -import org.schabi.newpipe.util.image.ImageStrategy; - -import static org.schabi.newpipe.database.LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM; -import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_NAME; -import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_SERVICE_ID; -import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_TABLE; -import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_URL; - -@Entity(tableName = REMOTE_PLAYLIST_TABLE, - indices = { - @Index(value = {REMOTE_PLAYLIST_SERVICE_ID, REMOTE_PLAYLIST_URL}, unique = true) - }) -public class PlaylistRemoteEntity implements PlaylistLocalItem { - public static final String REMOTE_PLAYLIST_TABLE = "remote_playlists"; - public static final String REMOTE_PLAYLIST_ID = "uid"; - public static final String REMOTE_PLAYLIST_SERVICE_ID = "service_id"; - public static final String REMOTE_PLAYLIST_NAME = "name"; - public static final String REMOTE_PLAYLIST_URL = "url"; - public static final String REMOTE_PLAYLIST_THUMBNAIL_URL = "thumbnail_url"; - public static final String REMOTE_PLAYLIST_UPLOADER_NAME = "uploader"; - public static final String REMOTE_PLAYLIST_DISPLAY_INDEX = "display_index"; - public static final String REMOTE_PLAYLIST_STREAM_COUNT = "stream_count"; - - @PrimaryKey(autoGenerate = true) - @ColumnInfo(name = REMOTE_PLAYLIST_ID) - private long uid = 0; - - @ColumnInfo(name = REMOTE_PLAYLIST_SERVICE_ID) - private int serviceId = Constants.NO_SERVICE_ID; - - @ColumnInfo(name = REMOTE_PLAYLIST_NAME) - private String name; - - @ColumnInfo(name = REMOTE_PLAYLIST_URL) - private String url; - - @ColumnInfo(name = REMOTE_PLAYLIST_THUMBNAIL_URL) - private String thumbnailUrl; - - @ColumnInfo(name = REMOTE_PLAYLIST_UPLOADER_NAME) - private String uploader; - - @ColumnInfo(name = REMOTE_PLAYLIST_DISPLAY_INDEX) - private long displayIndex = -1; // Make sure the new item is on the top - - @ColumnInfo(name = REMOTE_PLAYLIST_STREAM_COUNT) - private Long streamCount; - - public PlaylistRemoteEntity(final int serviceId, final String name, final String url, - final String thumbnailUrl, final String uploader, - final Long streamCount) { - this.serviceId = serviceId; - this.name = name; - this.url = url; - this.thumbnailUrl = thumbnailUrl; - this.uploader = uploader; - this.streamCount = streamCount; - } - - @Ignore - public PlaylistRemoteEntity(final int serviceId, final String name, final String url, - final String thumbnailUrl, final String uploader, - final long displayIndex, final Long streamCount) { - this.serviceId = serviceId; - this.name = name; - this.url = url; - this.thumbnailUrl = thumbnailUrl; - this.uploader = uploader; - this.displayIndex = displayIndex; - this.streamCount = streamCount; - } - - @Ignore - public PlaylistRemoteEntity(final PlaylistInfo info) { - this(info.getServiceId(), info.getName(), info.getUrl(), - // use uploader avatar when no thumbnail is available - ImageStrategy.imageListToDbUrl(info.getThumbnails().isEmpty() - ? info.getUploaderAvatars() : info.getThumbnails()), - info.getUploaderName(), info.getStreamCount()); - } - - @Ignore - public boolean isIdenticalTo(final PlaylistInfo info) { - /* - * Returns boolean comparing the online playlist and the local copy. - * (False if info changed such as playlist name or track count) - */ - return getServiceId() == info.getServiceId() - && getStreamCount() == info.getStreamCount() - && TextUtils.equals(getName(), info.getName()) - && TextUtils.equals(getUrl(), info.getUrl()) - // we want to update the local playlist data even when either the remote thumbnail - // URL changes, or the preferred image quality setting is changed by the user - && TextUtils.equals(getThumbnailUrl(), - ImageStrategy.imageListToDbUrl(info.getThumbnails())) - && TextUtils.equals(getUploader(), info.getUploaderName()); - } - - @Override - public long getUid() { - return uid; - } - - public void setUid(final long uid) { - this.uid = uid; - } - - public int getServiceId() { - return serviceId; - } - - public void setServiceId(final int serviceId) { - this.serviceId = serviceId; - } - - public String getName() { - return name; - } - - public void setName(final String name) { - this.name = name; - } - - @Nullable - @Override - public String getThumbnailUrl() { - return thumbnailUrl; - } - - public void setThumbnailUrl(final String thumbnailUrl) { - this.thumbnailUrl = thumbnailUrl; - } - - public String getUrl() { - return url; - } - - public void setUrl(final String url) { - this.url = url; - } - - public String getUploader() { - return uploader; - } - - public void setUploader(final String uploader) { - this.uploader = uploader; - } - - @Override - public long getDisplayIndex() { - return displayIndex; - } - - @Override - public void setDisplayIndex(final long displayIndex) { - this.displayIndex = displayIndex; - } - - public Long getStreamCount() { - return streamCount; - } - - public void setStreamCount(final Long streamCount) { - this.streamCount = streamCount; - } - - @Override - public LocalItemType getLocalItemType() { - return PLAYLIST_REMOTE_ITEM; - } - - @Override - public String getOrderingName() { - return name; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.kt new file mode 100644 index 000000000..82162e1e4 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.kt @@ -0,0 +1,104 @@ +/* + * SPDX-FileCopyrightText: 2018-2025 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database.playlist.model + +import android.text.TextUtils +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Ignore +import androidx.room.Index +import androidx.room.PrimaryKey +import org.schabi.newpipe.database.LocalItem.LocalItemType +import org.schabi.newpipe.database.playlist.PlaylistLocalItem +import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_SERVICE_ID +import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_TABLE +import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_URL +import org.schabi.newpipe.extractor.playlist.PlaylistInfo +import org.schabi.newpipe.util.NO_SERVICE_ID +import org.schabi.newpipe.util.image.ImageStrategy + +@Entity( + tableName = REMOTE_PLAYLIST_TABLE, + indices = [ + Index( + value = [REMOTE_PLAYLIST_SERVICE_ID, REMOTE_PLAYLIST_URL], + unique = true + ) + ] +) +data class PlaylistRemoteEntity( + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = REMOTE_PLAYLIST_ID) + override var uid: Long = 0, + + @ColumnInfo(name = REMOTE_PLAYLIST_SERVICE_ID) + val serviceId: Int = NO_SERVICE_ID, + + @ColumnInfo(name = REMOTE_PLAYLIST_NAME) + override val orderingName: String?, + + @ColumnInfo(name = REMOTE_PLAYLIST_URL) + val url: String?, + + @ColumnInfo(name = REMOTE_PLAYLIST_THUMBNAIL_URL) + override val thumbnailUrl: String?, + + @ColumnInfo(name = REMOTE_PLAYLIST_UPLOADER_NAME) + val uploader: String?, + + @ColumnInfo(name = REMOTE_PLAYLIST_DISPLAY_INDEX) + override var displayIndex: Long = -1, // Make sure the new item is on the top + + @ColumnInfo(name = REMOTE_PLAYLIST_STREAM_COUNT) + val streamCount: Long? +) : PlaylistLocalItem { + + constructor(playlistInfo: PlaylistInfo) : this( + serviceId = playlistInfo.serviceId, + orderingName = playlistInfo.name, + url = playlistInfo.url, + thumbnailUrl = ImageStrategy.imageListToDbUrl( + if (playlistInfo.thumbnails.isEmpty()) { + playlistInfo.uploaderAvatars + } else { + playlistInfo.thumbnails + } + ), + uploader = playlistInfo.uploaderName, + streamCount = playlistInfo.streamCount + ) + + override val localItemType: LocalItemType + get() = LocalItemType.PLAYLIST_REMOTE_ITEM + + /** + * Returns boolean comparing the online playlist and the local copy. + * (False if info changed such as playlist name or track count) + */ + @Ignore + fun isIdenticalTo(info: PlaylistInfo): Boolean { + return this.serviceId == info.serviceId && this.streamCount == info.streamCount && + TextUtils.equals(this.orderingName, info.name) && + TextUtils.equals(this.url, info.url) && + // we want to update the local playlist data even when either the remote thumbnail + // URL changes, or the preferred image quality setting is changed by the user + TextUtils.equals(thumbnailUrl, ImageStrategy.imageListToDbUrl(info.thumbnails)) && + TextUtils.equals(this.uploader, info.uploaderName) + } + + companion object { + const val REMOTE_PLAYLIST_TABLE = "remote_playlists" + const val REMOTE_PLAYLIST_ID = "uid" + const val REMOTE_PLAYLIST_SERVICE_ID = "service_id" + const val REMOTE_PLAYLIST_NAME = "name" + const val REMOTE_PLAYLIST_URL = "url" + const val REMOTE_PLAYLIST_THUMBNAIL_URL = "thumbnail_url" + const val REMOTE_PLAYLIST_UPLOADER_NAME = "uploader" + const val REMOTE_PLAYLIST_DISPLAY_INDEX = "display_index" + const val REMOTE_PLAYLIST_STREAM_COUNT = "stream_count" + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.java b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.java deleted file mode 100644 index f3208b6d5..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.java +++ /dev/null @@ -1,76 +0,0 @@ -package org.schabi.newpipe.database.playlist.model; - -import androidx.room.ColumnInfo; -import androidx.room.Entity; -import androidx.room.ForeignKey; -import androidx.room.Index; - -import org.schabi.newpipe.database.stream.model.StreamEntity; - -import static androidx.room.ForeignKey.CASCADE; -import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_INDEX; -import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_PLAYLIST_ID; -import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_STREAM_ID; -import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE; - -@Entity(tableName = PLAYLIST_STREAM_JOIN_TABLE, - primaryKeys = {JOIN_PLAYLIST_ID, JOIN_INDEX}, - indices = { - @Index(value = {JOIN_PLAYLIST_ID, JOIN_INDEX}, unique = true), - @Index(value = {JOIN_STREAM_ID}) - }, - foreignKeys = { - @ForeignKey(entity = PlaylistEntity.class, - parentColumns = PlaylistEntity.PLAYLIST_ID, - childColumns = JOIN_PLAYLIST_ID, - onDelete = CASCADE, onUpdate = CASCADE, deferred = true), - @ForeignKey(entity = StreamEntity.class, - parentColumns = StreamEntity.STREAM_ID, - childColumns = JOIN_STREAM_ID, - onDelete = CASCADE, onUpdate = CASCADE, deferred = true) - }) -public class PlaylistStreamEntity { - public static final String PLAYLIST_STREAM_JOIN_TABLE = "playlist_stream_join"; - public static final String JOIN_PLAYLIST_ID = "playlist_id"; - public static final String JOIN_STREAM_ID = "stream_id"; - public static final String JOIN_INDEX = "join_index"; - - @ColumnInfo(name = JOIN_PLAYLIST_ID) - private long playlistUid; - - @ColumnInfo(name = JOIN_STREAM_ID) - private long streamUid; - - @ColumnInfo(name = JOIN_INDEX) - private int index; - - public PlaylistStreamEntity(final long playlistUid, final long streamUid, final int index) { - this.playlistUid = playlistUid; - this.streamUid = streamUid; - this.index = index; - } - - public long getPlaylistUid() { - return playlistUid; - } - - public void setPlaylistUid(final long playlistUid) { - this.playlistUid = playlistUid; - } - - public long getStreamUid() { - return streamUid; - } - - public void setStreamUid(final long streamUid) { - this.streamUid = streamUid; - } - - public int getIndex() { - return index; - } - - public void setIndex(final int index) { - this.index = index; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.kt new file mode 100644 index 000000000..6ab1b6ac4 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.kt @@ -0,0 +1,68 @@ +/* + * SPDX-FileCopyrightText: 2018-2020 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database.playlist.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.ForeignKey.Companion.CASCADE +import androidx.room.Index +import org.schabi.newpipe.database.LocalItem +import org.schabi.newpipe.database.playlist.model.PlaylistEntity.Companion.PLAYLIST_ID +import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.Companion.JOIN_INDEX +import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.Companion.JOIN_PLAYLIST_ID +import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.Companion.JOIN_STREAM_ID +import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.Companion.PLAYLIST_STREAM_JOIN_TABLE +import org.schabi.newpipe.database.stream.model.StreamEntity + +@Entity( + tableName = PLAYLIST_STREAM_JOIN_TABLE, + primaryKeys = [JOIN_PLAYLIST_ID, JOIN_INDEX], + indices = [ + Index(value = [JOIN_PLAYLIST_ID, JOIN_INDEX], unique = true), + Index(value = [JOIN_STREAM_ID]) + ], + foreignKeys = [ + ForeignKey( + entity = PlaylistEntity::class, + parentColumns = arrayOf(PLAYLIST_ID), + childColumns = arrayOf(JOIN_PLAYLIST_ID), + onDelete = CASCADE, + onUpdate = CASCADE, + deferred = true + ), + ForeignKey( + entity = StreamEntity::class, + parentColumns = arrayOf(StreamEntity.STREAM_ID), + childColumns = arrayOf(JOIN_STREAM_ID), + onDelete = CASCADE, + onUpdate = CASCADE, + deferred = true + ) + ] +) +data class PlaylistStreamEntity( + @ColumnInfo(name = JOIN_PLAYLIST_ID) + val playlistUid: Long, + + @ColumnInfo(name = JOIN_STREAM_ID) + val streamUid: Long, + + @ColumnInfo(name = JOIN_INDEX) + val index: Int +) : LocalItem { + + override val localItemType: LocalItem.LocalItemType + get() = LocalItem.LocalItemType.PLAYLIST_STREAM_ITEM + + companion object { + const val PLAYLIST_STREAM_JOIN_TABLE = "playlist_stream_join" + const val JOIN_PLAYLIST_ID = "playlist_id" + const val JOIN_STREAM_ID = "stream_id" + const val JOIN_INDEX = "join_index" + } +} 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 index 1f3654e7a..3fa281e45 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.kt +++ b/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.kt @@ -1,16 +1,23 @@ +/* + * SPDX-FileCopyrightText: 2020-2023 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + package org.schabi.newpipe.database.stream import androidx.room.ColumnInfo import androidx.room.Embedded +import androidx.room.Ignore 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.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS +import org.schabi.newpipe.database.stream.model.StreamStateEntity.Companion.STREAM_PROGRESS_MILLIS import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.util.image.ImageStrategy import java.time.OffsetDateTime -class StreamStatisticsEntry( +data class StreamStatisticsEntry( @Embedded val streamEntity: StreamEntity, @@ -26,18 +33,23 @@ class StreamStatisticsEntry( @ColumnInfo(name = STREAM_WATCH_COUNT) val watchCount: Long ) : LocalItem { + + override val localItemType: LocalItem.LocalItemType + get() = LocalItem.LocalItemType.STATISTIC_STREAM_ITEM + + @Ignore fun toStreamInfoItem(): StreamInfoItem { - val item = StreamInfoItem(streamEntity.serviceId, streamEntity.url, streamEntity.title, streamEntity.streamType) - item.duration = streamEntity.duration - item.uploaderName = streamEntity.uploader - item.uploaderUrl = streamEntity.uploaderUrl - item.thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl) - - return item - } - - override fun getLocalItemType(): LocalItem.LocalItemType { - return LocalItem.LocalItemType.STATISTIC_STREAM_ITEM + return StreamInfoItem( + streamEntity.serviceId, + streamEntity.url, + streamEntity.title, + streamEntity.streamType + ).apply { + duration = streamEntity.duration + uploaderName = streamEntity.uploader + uploaderUrl = streamEntity.uploaderUrl + thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl) + } } companion object { diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java deleted file mode 100644 index 06371248d..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java +++ /dev/null @@ -1,48 +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.stream.model.StreamStateEntity; - -import java.util.List; - -import io.reactivex.rxjava3.core.Flowable; - -import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID; -import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE; - -@Dao -public interface StreamStateDAO extends BasicDAO { - @Override - @Query("SELECT * FROM " + STREAM_STATE_TABLE) - Flowable> getAll(); - - @Override - @Query("DELETE FROM " + STREAM_STATE_TABLE) - int deleteAll(); - - @Override - default Flowable> listByService(final int serviceId) { - throw new UnsupportedOperationException(); - } - - @Query("SELECT * FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId") - Flowable> getState(long streamId); - - @Query("DELETE FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId") - int deleteState(long streamId); - - @Insert(onConflict = OnConflictStrategy.IGNORE) - void silentInsertInternal(StreamStateEntity streamState); - - @Transaction - default long upsert(final StreamStateEntity stream) { - silentInsertInternal(stream); - return update(stream); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.kt b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.kt new file mode 100644 index 000000000..f3c44f1f2 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.kt @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: 2018-2021 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +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 io.reactivex.rxjava3.core.Flowable +import org.schabi.newpipe.database.BasicDAO +import org.schabi.newpipe.database.stream.model.StreamStateEntity + +@Dao +interface StreamStateDAO : BasicDAO { + + @Query("SELECT * FROM " + StreamStateEntity.STREAM_STATE_TABLE) + override fun getAll(): Flowable> + + @Query("DELETE FROM " + StreamStateEntity.STREAM_STATE_TABLE) + override fun deleteAll(): Int + + override fun listByService(serviceId: Int): Flowable> { + throw UnsupportedOperationException() + } + + @Query("SELECT * FROM " + StreamStateEntity.STREAM_STATE_TABLE + " WHERE " + StreamStateEntity.JOIN_STREAM_ID + " = :streamId") + fun getState(streamId: Long): Flowable> + + @Query("DELETE FROM " + StreamStateEntity.STREAM_STATE_TABLE + " WHERE " + StreamStateEntity.JOIN_STREAM_ID + " = :streamId") + fun deleteState(streamId: Long): Int + + @Insert(onConflict = OnConflictStrategy.Companion.IGNORE) + fun silentInsertInternal(streamState: StreamStateEntity) + + @Transaction + fun upsert(stream: StreamStateEntity): Long { + silentInsertInternal(stream) + return update(stream).toLong() + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java deleted file mode 100644 index 627acea45..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java +++ /dev/null @@ -1,112 +0,0 @@ -package org.schabi.newpipe.database.stream.model; - -import androidx.annotation.Nullable; -import androidx.room.ColumnInfo; -import androidx.room.Entity; -import androidx.room.ForeignKey; - -import java.util.Objects; - -import static androidx.room.ForeignKey.CASCADE; -import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID; -import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE; - -@Entity(tableName = STREAM_STATE_TABLE, - primaryKeys = {JOIN_STREAM_ID}, - foreignKeys = { - @ForeignKey(entity = StreamEntity.class, - parentColumns = StreamEntity.STREAM_ID, - childColumns = JOIN_STREAM_ID, - onDelete = CASCADE, onUpdate = CASCADE) - }) -public class StreamStateEntity { - public static final String STREAM_STATE_TABLE = "stream_state"; - public static final String JOIN_STREAM_ID = "stream_id"; - // This additional field is required for the SQL query because 'stream_id' is used - // for some other joins already - public static final String JOIN_STREAM_ID_ALIAS = "stream_id_alias"; - public static final String STREAM_PROGRESS_MILLIS = "progress_time"; - - /** - * Playback state will not be saved, if playback time is less than this threshold (5000ms = 5s). - */ - public static final long PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS = 5000; - - /** - * Stream will be considered finished if the playback time left exceeds this threshold - * (60000ms = 60s). - * @see #isFinished(long) - * @see org.schabi.newpipe.database.feed.dao.FeedDAO#getLiveOrNotPlayedStreams() - * @see org.schabi.newpipe.database.feed.dao.FeedDAO#getLiveOrNotPlayedStreamsForGroup(long) - */ - public static final long PLAYBACK_FINISHED_END_MILLISECONDS = 60000; - - @ColumnInfo(name = JOIN_STREAM_ID) - private long streamUid; - - @ColumnInfo(name = STREAM_PROGRESS_MILLIS) - private long progressMillis; - - public StreamStateEntity(final long streamUid, final long progressMillis) { - this.streamUid = streamUid; - this.progressMillis = progressMillis; - } - - public long getStreamUid() { - return streamUid; - } - - public void setStreamUid(final long streamUid) { - this.streamUid = streamUid; - } - - public long getProgressMillis() { - return progressMillis; - } - - public void setProgressMillis(final long progressMillis) { - this.progressMillis = progressMillis; - } - - /** - * The state will be considered valid, and thus be saved, if the progress is more than {@link - * #PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS} or at least 1/4 of the video length. - * @param durationInSeconds the duration of the stream connected with this state, in seconds - * @return whether this stream state entity should be saved or not - */ - public boolean isValid(final long durationInSeconds) { - return progressMillis > PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS - || progressMillis > durationInSeconds * 1000 / 4; - } - - /** - * The video will be considered as finished, if the time left is less than {@link - * #PLAYBACK_FINISHED_END_MILLISECONDS} and the progress is at least 3/4 of the video length. - * The state will be saved anyway, so that it can be shown under stream info items, but the - * player will not resume if a state is considered as finished. Finished streams are also the - * ones that can be filtered out in the feed fragment. - * @see org.schabi.newpipe.database.feed.dao.FeedDAO#getLiveOrNotPlayedStreams() - * @see org.schabi.newpipe.database.feed.dao.FeedDAO#getLiveOrNotPlayedStreamsForGroup(long) - * @param durationInSeconds the duration of the stream connected with this state, in seconds - * @return whether the stream is finished or not - */ - public boolean isFinished(final long durationInSeconds) { - return progressMillis >= durationInSeconds * 1000 - PLAYBACK_FINISHED_END_MILLISECONDS - && progressMillis >= durationInSeconds * 1000 * 3 / 4; - } - - @Override - public boolean equals(@Nullable final Object obj) { - if (obj instanceof StreamStateEntity) { - return ((StreamStateEntity) obj).streamUid == streamUid - && ((StreamStateEntity) obj).progressMillis == progressMillis; - } else { - return false; - } - } - - @Override - public int hashCode() { - return Objects.hash(streamUid, progressMillis); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.kt b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.kt new file mode 100644 index 000000000..759a2dcec --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.kt @@ -0,0 +1,85 @@ +/* + * SPDX-FileCopyrightText: 2018-2023 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database.stream.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.ForeignKey.Companion.CASCADE +import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID +import org.schabi.newpipe.database.stream.model.StreamStateEntity.Companion.JOIN_STREAM_ID +import org.schabi.newpipe.database.stream.model.StreamStateEntity.Companion.PLAYBACK_FINISHED_END_MILLISECONDS +import org.schabi.newpipe.database.stream.model.StreamStateEntity.Companion.STREAM_STATE_TABLE + +@Entity( + tableName = STREAM_STATE_TABLE, + primaryKeys = [JOIN_STREAM_ID], + foreignKeys = [ + ForeignKey( + entity = StreamEntity::class, + parentColumns = arrayOf(STREAM_ID), + childColumns = arrayOf(JOIN_STREAM_ID), + onDelete = CASCADE, + onUpdate = CASCADE + ) + ] +) +data class StreamStateEntity( + @ColumnInfo(name = JOIN_STREAM_ID) + val streamUid: Long, + + @ColumnInfo(name = STREAM_PROGRESS_MILLIS) + val progressMillis: Long +) { + /** + * The state will be considered valid, and thus be saved, if the progress is more than + * [PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS] or at least 1/4 of the video length. + * @param durationInSeconds the duration of the stream connected with this state, in seconds + * @return whether this stream state entity should be saved or not + */ + fun isValid(durationInSeconds: Long): Boolean { + return progressMillis > PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS || + progressMillis > durationInSeconds * 1000 / 4 + } + + /** + * The video will be considered as finished, if the time left is less than + * [PLAYBACK_FINISHED_END_MILLISECONDS] and the progress is at least 3/4 of the video length. + * The state will be saved anyway, so that it can be shown under stream info items, but the + * player will not resume if a state is considered as finished. Finished streams are also the + * ones that can be filtered out in the feed fragment. + * @param durationInSeconds the duration of the stream connected with this state, in seconds + * @return whether the stream is finished or not + */ + fun isFinished(durationInSeconds: Long): Boolean { + return progressMillis >= durationInSeconds * 1000 - PLAYBACK_FINISHED_END_MILLISECONDS && + progressMillis >= durationInSeconds * 1000 * 3 / 4 + } + + companion object { + const val STREAM_STATE_TABLE = "stream_state" + const val JOIN_STREAM_ID = "stream_id" + + // This additional field is required for the SQL query because 'stream_id' is used + // for some other joins already + const val JOIN_STREAM_ID_ALIAS = "stream_id_alias" + const val STREAM_PROGRESS_MILLIS = "progress_time" + + /** + * Playback state will not be saved, if playback time is less than this threshold + * (5000ms = 5s). + */ + const val PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS = 5000L + + /** + * Stream will be considered finished if the playback time left exceeds this threshold + * (60000ms = 60s). + * @see org.schabi.newpipe.database.stream.model.StreamStateEntity.isFinished + */ + const val PLAYBACK_FINISHED_END_MILLISECONDS = 60000L + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/NotificationMode.java b/app/src/main/java/org/schabi/newpipe/database/subscription/NotificationMode.java deleted file mode 100644 index 07e0eb7d3..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/subscription/NotificationMode.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.schabi.newpipe.database.subscription; - -import androidx.annotation.IntDef; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -@IntDef({NotificationMode.DISABLED, NotificationMode.ENABLED}) -@Retention(RetentionPolicy.SOURCE) -public @interface NotificationMode { - - int DISABLED = 0; - int ENABLED = 1; - //other values reserved for the future -} diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/NotificationMode.kt b/app/src/main/java/org/schabi/newpipe/database/subscription/NotificationMode.kt new file mode 100644 index 000000000..f9bb18c0c --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/subscription/NotificationMode.kt @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2021 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database.subscription + +import androidx.annotation.IntDef + +@IntDef(NotificationMode.Companion.DISABLED, NotificationMode.Companion.ENABLED) +@Retention(AnnotationRetention.SOURCE) +annotation class NotificationMode { + companion object { + const val DISABLED = 0 + const val ENABLED = 1 // other values reserved for the future + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt index 47b6f4dd9..e6fdcbf70 100644 --- a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt +++ b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt @@ -99,7 +99,7 @@ abstract class SubscriptionDAO : BasicDAO { if (uidFromInsert != -1L) { entity.uid = uidFromInsert } else { - val subscriptionIdFromDb = getSubscriptionIdInternal(entity.serviceId, entity.url) + val subscriptionIdFromDb = getSubscriptionIdInternal(entity.serviceId, entity.url!!) ?: throw IllegalStateException("Subscription cannot be null just after insertion.") entity.uid = subscriptionIdFromDb 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 deleted file mode 100644 index df5a3067a..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java +++ /dev/null @@ -1,200 +0,0 @@ -package org.schabi.newpipe.database.subscription; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -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.channel.ChannelInfo; -import org.schabi.newpipe.extractor.channel.ChannelInfoItem; -import org.schabi.newpipe.util.Constants; -import org.schabi.newpipe.util.image.ImageStrategy; - -import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_SERVICE_ID; -import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_TABLE; -import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_URL; - -@Entity(tableName = SUBSCRIPTION_TABLE, - indices = {@Index(value = {SUBSCRIPTION_SERVICE_ID, SUBSCRIPTION_URL}, unique = true)}) -public class SubscriptionEntity { - public static final String SUBSCRIPTION_UID = "uid"; - public static final String SUBSCRIPTION_TABLE = "subscriptions"; - public static final String SUBSCRIPTION_SERVICE_ID = "service_id"; - public static final String SUBSCRIPTION_URL = "url"; - public static final String SUBSCRIPTION_NAME = "name"; - public static final String SUBSCRIPTION_AVATAR_URL = "avatar_url"; - public static final String SUBSCRIPTION_SUBSCRIBER_COUNT = "subscriber_count"; - public static final String SUBSCRIPTION_DESCRIPTION = "description"; - public static final String SUBSCRIPTION_NOTIFICATION_MODE = "notification_mode"; - - @PrimaryKey(autoGenerate = true) - private long uid = 0; - - @ColumnInfo(name = SUBSCRIPTION_SERVICE_ID) - private int serviceId = Constants.NO_SERVICE_ID; - - @ColumnInfo(name = SUBSCRIPTION_URL) - private String url; - - @ColumnInfo(name = SUBSCRIPTION_NAME) - private String name; - - @ColumnInfo(name = SUBSCRIPTION_AVATAR_URL) - private String avatarUrl; - - @ColumnInfo(name = SUBSCRIPTION_SUBSCRIBER_COUNT) - private Long subscriberCount; - - @ColumnInfo(name = SUBSCRIPTION_DESCRIPTION) - private String description; - - @ColumnInfo(name = SUBSCRIPTION_NOTIFICATION_MODE) - private int notificationMode; - - @Ignore - public static SubscriptionEntity from(@NonNull final ChannelInfo info) { - final SubscriptionEntity result = new SubscriptionEntity(); - result.setServiceId(info.getServiceId()); - result.setUrl(info.getUrl()); - result.setData(info.getName(), ImageStrategy.imageListToDbUrl(info.getAvatars()), - info.getDescription(), info.getSubscriberCount()); - return result; - } - - public long getUid() { - return uid; - } - - public void setUid(final long uid) { - this.uid = uid; - } - - public int getServiceId() { - return serviceId; - } - - public void setServiceId(final int serviceId) { - this.serviceId = serviceId; - } - - public String getUrl() { - return url; - } - - public void setUrl(final String url) { - this.url = url; - } - - public String getName() { - return name; - } - - public void setName(final String name) { - this.name = name; - } - - @Nullable - public String getAvatarUrl() { - return avatarUrl; - } - - public void setAvatarUrl(@Nullable final String avatarUrl) { - this.avatarUrl = avatarUrl; - } - - public Long getSubscriberCount() { - return subscriberCount; - } - - public void setSubscriberCount(final Long subscriberCount) { - this.subscriberCount = subscriberCount; - } - - public String getDescription() { - return description; - } - - public void setDescription(final String description) { - this.description = description; - } - - @NotificationMode - public int getNotificationMode() { - return notificationMode; - } - - public void setNotificationMode(@NotificationMode final int notificationMode) { - this.notificationMode = notificationMode; - } - - @Ignore - public void setData(final String n, final String au, final String d, final Long sc) { - this.setName(n); - this.setAvatarUrl(au); - this.setDescription(d); - this.setSubscriberCount(sc); - } - - @Ignore - public ChannelInfoItem toChannelInfoItem() { - final ChannelInfoItem item = new ChannelInfoItem(getServiceId(), getUrl(), getName()); - item.setThumbnails(ImageStrategy.dbUrlToImageList(getAvatarUrl())); - item.setSubscriberCount(getSubscriberCount()); - item.setDescription(getDescription()); - return item; - } - - - // TODO: Remove these generated methods by migrating this class to a data class from Kotlin. - @Override - @SuppressWarnings("EqualsReplaceableByObjectsCall") - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - final SubscriptionEntity that = (SubscriptionEntity) o; - - if (uid != that.uid) { - return false; - } - if (serviceId != that.serviceId) { - return false; - } - if (!url.equals(that.url)) { - return false; - } - if (name != null ? !name.equals(that.name) : that.name != null) { - return false; - } - if (avatarUrl != null ? !avatarUrl.equals(that.avatarUrl) : that.avatarUrl != null) { - return false; - } - if (subscriberCount != null - ? !subscriberCount.equals(that.subscriberCount) - : that.subscriberCount != null) { - return false; - } - return description != null - ? description.equals(that.description) - : that.description == null; - } - - @Override - public int hashCode() { - int result = (int) (uid ^ (uid >>> 32)); - result = 31 * result + serviceId; - result = 31 * result + url.hashCode(); - result = 31 * result + (name != null ? name.hashCode() : 0); - result = 31 * result + (avatarUrl != null ? avatarUrl.hashCode() : 0); - result = 31 * result + (subscriberCount != null ? subscriberCount.hashCode() : 0); - result = 31 * result + (description != null ? description.hashCode() : 0); - return result; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.kt b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.kt new file mode 100644 index 000000000..104d0a142 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.kt @@ -0,0 +1,87 @@ +/* + * SPDX-FileCopyrightText: 2017-2024 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database.subscription + +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.channel.ChannelInfo +import org.schabi.newpipe.extractor.channel.ChannelInfoItem +import org.schabi.newpipe.util.NO_SERVICE_ID +import org.schabi.newpipe.util.image.ImageStrategy + +@Entity( + tableName = SubscriptionEntity.Companion.SUBSCRIPTION_TABLE, + indices = [ + Index( + value = [SubscriptionEntity.Companion.SUBSCRIPTION_SERVICE_ID, SubscriptionEntity.Companion.SUBSCRIPTION_URL], + unique = true + ) + ] +) +data class SubscriptionEntity( + @PrimaryKey(autoGenerate = true) + var uid: Long = 0, + + @ColumnInfo(name = SUBSCRIPTION_SERVICE_ID) + var serviceId: Int = NO_SERVICE_ID, + + @ColumnInfo(name = SUBSCRIPTION_URL) + var url: String? = null, + + @ColumnInfo(name = SUBSCRIPTION_NAME) + var name: String? = null, + + @ColumnInfo(name = SUBSCRIPTION_AVATAR_URL) + var avatarUrl: String? = null, + + @ColumnInfo(name = SUBSCRIPTION_SUBSCRIBER_COUNT) + var subscriberCount: Long? = null, + + @ColumnInfo(name = SUBSCRIPTION_DESCRIPTION) + var description: String? = null, + + @get:NotificationMode + @ColumnInfo(name = SUBSCRIPTION_NOTIFICATION_MODE) + var notificationMode: Int = 0 +) { + @Ignore + fun toChannelInfoItem(): ChannelInfoItem { + return ChannelInfoItem(this.serviceId, this.url, this.name).apply { + thumbnails = ImageStrategy.dbUrlToImageList(this@SubscriptionEntity.avatarUrl) + subscriberCount = this.subscriberCount + description = this.description + } + } + + companion object { + const val SUBSCRIPTION_UID: String = "uid" + const val SUBSCRIPTION_TABLE: String = "subscriptions" + const val SUBSCRIPTION_SERVICE_ID: String = "service_id" + const val SUBSCRIPTION_URL: String = "url" + const val SUBSCRIPTION_NAME: String = "name" + const val SUBSCRIPTION_AVATAR_URL: String = "avatar_url" + const val SUBSCRIPTION_SUBSCRIBER_COUNT: String = "subscriber_count" + const val SUBSCRIPTION_DESCRIPTION: String = "description" + const val SUBSCRIPTION_NOTIFICATION_MODE: String = "notification_mode" + + @JvmStatic + @Ignore + fun from(info: ChannelInfo): SubscriptionEntity { + return SubscriptionEntity( + serviceId = info.serviceId, + url = info.url, + name = info.name, + avatarUrl = ImageStrategy.imageListToDbUrl(info.avatars), + description = info.description, + subscriberCount = info.subscriberCount + ) + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index d36fc5fc9..d75d14b4a 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -361,10 +361,10 @@ public class ChannelFragment extends BaseStateFragment final SubscriptionEntity channel = new SubscriptionEntity(); channel.setServiceId(info.getServiceId()); channel.setUrl(info.getUrl()); - channel.setData(info.getName(), - ImageStrategy.imageListToDbUrl(info.getAvatars()), - info.getDescription(), - info.getSubscriberCount()); + channel.setName(info.getName()); + channel.setAvatarUrl(ImageStrategy.imageListToDbUrl(info.getAvatars())); + channel.setDescription(info.getDescription()); + channel.setSubscriberCount(info.getSubscriberCount()); channelSubscription = null; updateNotifyButton(null); subscribeButtonMonitor = monitorSubscribeButton(mapOnSubscribe(channel)); diff --git a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java index a5e1594d1..d15db84c7 100644 --- a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java @@ -140,7 +140,7 @@ public final class BookmarkFragment extends BaseLocalListFragment playlists) { return playlists.stream() - .anyMatch(playlist -> playlist.timesStreamIsContained > 0); + .anyMatch(playlist -> playlist.getTimesStreamIsContained() > 0); } private void onPlaylistSelected(@NonNull final LocalPlaylistManager manager, @@ -146,9 +146,9 @@ public final class PlaylistAppendDialog extends PlaylistDialog { @NonNull final List streams) { final String toastText; - if (playlist.timesStreamIsContained > 0) { + if (playlist.getTimesStreamIsContained() > 0) { toastText = getString(R.string.playlist_add_stream_success_duplicate, - playlist.timesStreamIsContained); + playlist.getTimesStreamIsContained()); } else { toastText = getString(R.string.playlist_add_stream_success); } @@ -160,8 +160,9 @@ public final class PlaylistAppendDialog extends PlaylistDialog { .subscribe(ignored -> { successToast.show(); - if (playlist.thumbnailUrl != null - && playlist.thumbnailUrl.equals(PlaylistEntity.DEFAULT_THUMBNAIL)) { + if (playlist.getThumbnailUrl() != null + && playlist.getThumbnailUrl().equals(PlaylistEntity.DEFAULT_THUMBNAIL) + ) { playlistDisposables.add(manager .changePlaylistThumbnail(playlist.getUid(), streams.get(0).getUid(), false) 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 91f98f5d2..bbad7f689 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 @@ -507,7 +507,7 @@ class FeedFragment : BaseStateFragment() { .setTitle(R.string.feed_load_error) .setPositiveButton(R.string.unsubscribe) { _, _ -> SubscriptionManager(requireContext()) - .deleteSubscription(subscriptionEntity.serviceId, subscriptionEntity.url) + .deleteSubscription(subscriptionEntity.serviceId, subscriptionEntity.url!!) .subscribe() handleItemsErrors(nextItemsErrors) } diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java index 336f5cfe3..528275d75 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java @@ -35,15 +35,15 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder { } final PlaylistMetadataEntry item = (PlaylistMetadataEntry) localItem; - itemTitleView.setText(item.name); + itemTitleView.setText(item.getOrderingName()); itemStreamCountView.setText(Localization.localizeStreamCountMini( - itemStreamCountView.getContext(), item.streamCount)); + itemStreamCountView.getContext(), item.getStreamCount())); itemUploaderView.setVisibility(View.INVISIBLE); - PicassoHelper.loadPlaylistThumbnail(item.thumbnailUrl).into(itemThumbnailView); + PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView); if (item instanceof PlaylistDuplicatesEntry - && ((PlaylistDuplicatesEntry) item).timesStreamIsContained > 0) { + && ((PlaylistDuplicatesEntry) item).getTimesStreamIsContained() > 0) { itemView.setAlpha(GRAYED_OUT_ALPHA); } else { itemView.setAlpha(1.0f); diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java index 765732063..3a339aec8 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java @@ -34,7 +34,7 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder { } final PlaylistRemoteEntity item = (PlaylistRemoteEntity) localItem; - itemTitleView.setText(item.getName()); + itemTitleView.setText(item.getOrderingName()); itemStreamCountView.setText(Localization.localizeStreamCountMini( itemStreamCountView.getContext(), item.getStreamCount())); // Here is where the uploader name is set in the bookmarked playlists library diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistManager.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistManager.java index dd9307675..1480735fb 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistManager.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistManager.java @@ -148,7 +148,7 @@ public class LocalPlaylistManager { public boolean getIsPlaylistThumbnailPermanent(final long playlistId) { return playlistTable.getPlaylist(playlistId).blockingFirst().get(0) - .getIsThumbnailPermanent(); + .isThumbnailPermanent(); } public long getAutomaticPlaylistThumbnailStreamId(final long playlistId) { @@ -174,7 +174,7 @@ public class LocalPlaylistManager { } if (thumbnailStreamId != THUMBNAIL_ID_LEAVE_UNCHANGED) { playlist.setThumbnailStreamId(thumbnailStreamId); - playlist.setIsThumbnailPermanent(isPermanent); + playlist.setThumbnailPermanent(isPermanent); } return playlistTable.update(playlist); }).subscribeOn(Schedulers.io()); diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt index 474add4f4..c0783e812 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt @@ -26,7 +26,7 @@ class SubscriptionManager(context: Context) { private val feedDatabaseManager = FeedDatabaseManager(context) fun subscriptionTable(): SubscriptionDAO = subscriptionTable - fun subscriptions() = subscriptionTable.all + fun subscriptions() = subscriptionTable.getAll() fun getSubscriptions( currentGroupId: Long = FeedGroupEntity.GROUP_ALL_ID, @@ -44,7 +44,7 @@ class SubscriptionManager(context: Context) { } } showOnlyUngrouped -> subscriptionTable.getSubscriptionsOnlyUngrouped(currentGroupId) - else -> subscriptionTable.all + else -> subscriptionTable.getAll() } } @@ -71,12 +71,12 @@ class SubscriptionManager(context: Context) { subscriptionTable.getSubscription(info.serviceId, info.url) .flatMapCompletable { Completable.fromRunnable { - it.setData( - info.name, - ImageStrategy.imageListToDbUrl(info.avatars), - info.description, - info.subscriberCount - ) + it.apply { + name = info.name + avatarUrl = ImageStrategy.imageListToDbUrl(info.avatars) + description = info.description + subscriberCount = info.subscriberCount + } subscriptionTable.update(it) } } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserImpl.kt b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserImpl.kt index b7d57657d..d221d704b 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserImpl.kt +++ b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserImpl.kt @@ -315,7 +315,7 @@ class MediaBrowserImpl( } private fun populateHistory(): Single> { - val history = database.streamHistoryDAO().getHistory().firstOrError() + val history = database.streamHistoryDAO().history.firstOrError() return history.map { items -> items.map { this.createHistoryMediaItem(it) } } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserPlaybackPreparer.kt b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserPlaybackPreparer.kt index 4815965a3..072a8f332 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserPlaybackPreparer.kt +++ b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserPlaybackPreparer.kt @@ -215,7 +215,7 @@ class MediaBrowserPlaybackPreparer( } val streamId = path[0].toLong() - return database.streamHistoryDAO().getHistory() + return database.streamHistoryDAO().history .firstOrError() .map { items -> val infoItems = items diff --git a/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java index 6a5f7c894..880cbb282 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java @@ -118,12 +118,12 @@ public class SelectPlaylistFragment extends DialogFragment { if (selectedItem instanceof PlaylistMetadataEntry) { final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem); - onSelectedListener.onLocalPlaylistSelected(entry.getUid(), entry.name); + onSelectedListener.onLocalPlaylistSelected(entry.getUid(), entry.getOrderingName()); } else if (selectedItem instanceof PlaylistRemoteEntity) { final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem); onSelectedListener.onRemotePlaylistSelected( - entry.getServiceId(), entry.getUrl(), entry.getName()); + entry.getServiceId(), entry.getUrl(), entry.getOrderingName()); } } dismiss(); @@ -157,14 +157,15 @@ public class SelectPlaylistFragment extends DialogFragment { if (selectedItem instanceof PlaylistMetadataEntry) { final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem); - holder.titleView.setText(entry.name); + holder.titleView.setText(entry.getOrderingName()); holder.view.setOnClickListener(view -> clickedItem(position)); - PicassoHelper.loadPlaylistThumbnail(entry.thumbnailUrl).into(holder.thumbnailView); + PicassoHelper.loadPlaylistThumbnail(entry.getThumbnailUrl()) + .into(holder.thumbnailView); } else if (selectedItem instanceof PlaylistRemoteEntity) { final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem); - holder.titleView.setText(entry.getName()); + holder.titleView.setText(entry.getOrderingName()); holder.view.setOnClickListener(view -> clickedItem(position)); PicassoHelper.loadPlaylistThumbnail(entry.getThumbnailUrl()) .into(holder.thumbnailView); diff --git a/app/src/main/java/org/schabi/newpipe/settings/notifications/NotificationModeConfigAdapter.kt b/app/src/main/java/org/schabi/newpipe/settings/notifications/NotificationModeConfigAdapter.kt index f61aa72ab..fd8abfa16 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/notifications/NotificationModeConfigAdapter.kt +++ b/app/src/main/java/org/schabi/newpipe/settings/notifications/NotificationModeConfigAdapter.kt @@ -31,7 +31,7 @@ class NotificationModeConfigAdapter( fun update(newData: List) { val items = newData.map { - SubscriptionItem(it.uid, it.name, it.notificationMode, it.serviceId, it.url) + SubscriptionItem(it.uid, it.name!!, it.notificationMode, it.serviceId, it.url!!) } submitList(items) }