From 4422b55ab402c460fe8984f02375aca02675dbbc Mon Sep 17 00:00:00 2001 From: Aayush Gupta Date: Fri, 24 Oct 2025 19:05:57 +0800 Subject: [PATCH 1/6] Migrate database logic to Kotlin Room has been convereted into a KMP library in the latest stable releases and annotation processing requires KSP which only generates kotlin classes Signed-off-by: Aayush Gupta --- .../9.json | 2 +- .../org/schabi/newpipe/NewPipeDatabase.java | 72 ---- .../org/schabi/newpipe/NewPipeDatabase.kt | 80 ++++ .../schabi/newpipe/database/AppDatabase.java | 65 ---- .../schabi/newpipe/database/AppDatabase.kt | 68 ++++ .../org/schabi/newpipe/database/BasicDAO.java | 39 -- .../org/schabi/newpipe/database/BasicDAO.kt | 42 ++ .../schabi/newpipe/database/LocalItem.java | 13 - .../org/schabi/newpipe/database/LocalItem.kt | 19 + .../schabi/newpipe/database/Migrations.java | 307 --------------- .../org/schabi/newpipe/database/Migrations.kt | 368 ++++++++++++++++++ .../database/history/dao/HistoryDAO.java | 7 - .../database/history/dao/HistoryDAO.kt | 13 + .../history/dao/SearchHistoryDAO.java | 52 --- .../database/history/dao/SearchHistoryDAO.kt | 42 ++ .../history/dao/StreamHistoryDAO.java | 89 ----- .../database/history/dao/StreamHistoryDAO.kt | 63 +++ .../history/model/SearchHistoryEntry.kt | 31 +- .../history/model/StreamHistoryEntity.java | 81 ---- .../history/model/StreamHistoryEntity.kt | 56 +++ .../playlist/PlaylistDuplicatesEntry.java | 29 -- .../playlist/PlaylistDuplicatesEntry.kt | 40 ++ .../database/playlist/PlaylistLocalItem.java | 18 - .../database/playlist/PlaylistLocalItem.kt | 16 + .../playlist/PlaylistMetadataEntry.java | 82 ---- .../playlist/PlaylistMetadataEntry.kt | 42 ++ .../database/playlist/PlaylistStreamEntry.kt | 31 +- .../database/playlist/dao/PlaylistDAO.java | 53 --- .../database/playlist/dao/PlaylistDAO.kt | 48 +++ .../playlist/dao/PlaylistRemoteDAO.java | 68 ---- .../playlist/dao/PlaylistRemoteDAO.kt | 55 +++ .../playlist/dao/PlaylistStreamDAO.java | 159 -------- .../playlist/dao/PlaylistStreamDAO.kt | 151 +++++++ .../playlist/model/PlaylistEntity.java | 100 ----- .../database/playlist/model/PlaylistEntity.kt | 58 +++ .../playlist/model/PlaylistRemoteEntity.java | 191 --------- .../playlist/model/PlaylistRemoteEntity.kt | 104 +++++ .../playlist/model/PlaylistStreamEntity.java | 76 ---- .../playlist/model/PlaylistStreamEntity.kt | 68 ++++ .../database/stream/StreamStatisticsEntry.kt | 38 +- .../database/stream/dao/StreamStateDAO.java | 48 --- .../database/stream/dao/StreamStateDAO.kt | 45 +++ .../stream/model/StreamStateEntity.java | 112 ------ .../stream/model/StreamStateEntity.kt | 85 ++++ .../subscription/NotificationMode.java | 14 - .../database/subscription/NotificationMode.kt | 18 + .../database/subscription/SubscriptionDAO.kt | 2 +- .../subscription/SubscriptionEntity.java | 200 ---------- .../subscription/SubscriptionEntity.kt | 87 +++++ .../list/channel/ChannelFragment.java | 8 +- .../local/bookmark/BookmarkFragment.java | 14 +- .../local/dialog/PlaylistAppendDialog.java | 11 +- .../schabi/newpipe/local/feed/FeedFragment.kt | 2 +- .../local/holder/LocalPlaylistItemHolder.java | 8 +- .../holder/RemotePlaylistItemHolder.java | 2 +- .../local/playlist/LocalPlaylistManager.java | 4 +- .../local/subscription/SubscriptionManager.kt | 16 +- .../player/mediabrowser/MediaBrowserImpl.kt | 2 +- .../MediaBrowserPlaybackPreparer.kt | 2 +- .../settings/SelectPlaylistFragment.java | 11 +- .../NotificationModeConfigAdapter.kt | 2 +- 61 files changed, 1676 insertions(+), 1953 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java create mode 100644 app/src/main/java/org/schabi/newpipe/NewPipeDatabase.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/database/AppDatabase.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/AppDatabase.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/database/BasicDAO.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/BasicDAO.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/database/LocalItem.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/LocalItem.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/database/Migrations.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/Migrations.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/database/history/dao/HistoryDAO.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/history/dao/HistoryDAO.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntity.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntity.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistDuplicatesEntry.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistDuplicatesEntry.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/database/subscription/NotificationMode.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/subscription/NotificationMode.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.kt 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) } From 05b9ff49a2e9f35f4851ffc74ef527f558a5d4ec Mon Sep 17 00:00:00 2001 From: Aayush Gupta Date: Mon, 27 Oct 2025 23:04:01 +0800 Subject: [PATCH 2/6] Migrate from KAPT to KSP Ref: https://developer.android.com/build/migrate-to-ksp Signed-off-by: Aayush Gupta --- app/build.gradle.kts | 19 +++++++++---------- build.gradle.kts | 2 +- gradle/libs.versions.toml | 3 ++- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 037c3cb53..e4f318f65 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -6,7 +6,7 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.jetbrains.kotlin.android) - alias(libs.plugins.jetbrains.kotlin.kapt) + alias(libs.plugins.google.ksp) alias(libs.plugins.jetbrains.kotlin.parcelize) alias(libs.plugins.sonarqube) checkstyle @@ -40,12 +40,6 @@ android { System.getProperty("versionNameSuffix")?.let { versionNameSuffix = it } testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - - javaCompileOptions { - annotationProcessorOptions { - arguments["room.schemaLocation"] = "$projectDir/schemas" - } - } } buildTypes { @@ -124,6 +118,11 @@ android { } } +ksp { + arg("room.schemaLocation", "$projectDir/schemas") +} + + // Custom dependency configuration for ktlint val ktlint by configurations.creating @@ -218,7 +217,7 @@ dependencies { implementation(libs.androidx.recyclerview) implementation(libs.androidx.room.runtime) implementation(libs.androidx.room.rxjava3) - kapt(libs.androidx.room.compiler) + ksp(libs.androidx.room.compiler) implementation(libs.androidx.swiperefreshlayout) implementation(libs.androidx.viewpager2) implementation(libs.androidx.work.runtime) @@ -229,7 +228,7 @@ dependencies { /** Third-party libraries **/ implementation(libs.livefront.bridge) implementation(libs.evernote.statesaver.core) - kapt(libs.evernote.statesaver.compiler) + ksp(libs.evernote.statesaver.compiler) // HTML parser implementation(libs.jsoup) @@ -249,7 +248,7 @@ dependencies { // Metadata generator for service descriptors compileOnly(libs.google.autoservice.annotations) - kapt(libs.google.autoservice.compiler) + ksp(libs.google.autoservice.compiler) // Manager for complex RecyclerView layouts implementation(libs.lisawray.groupie.core) diff --git a/build.gradle.kts b/build.gradle.kts index e79ccc3e0..d16afb3af 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.jetbrains.kotlin.android) apply false - alias(libs.plugins.jetbrains.kotlin.kapt) apply false + alias(libs.plugins.google.ksp) apply false alias(libs.plugins.jetbrains.kotlin.parcelize) apply false alias(libs.plugins.sonarqube) apply false } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f48678867..cd54e5ef4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,6 +23,7 @@ jsoup = "1.21.2" junit = "4.13.2" junit-ext = "1.1.5" kotlin = "1.9.25" +ksp = "1.9.25-1.0.20" ktlint = "0.45.2" leakcanary = "2.12" lifecycle = "2.6.2" @@ -130,7 +131,7 @@ squareup-picasso = { module = "com.squareup.picasso:picasso", version.ref = "pic [plugins] android-application = { id = "com.android.application", version.ref = "agp" } +google-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } -jetbrains-kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } jetbrains-kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } sonarqube = { id = "org.sonarqube", version.ref = "sonarqube" } From 995a92b7a44a770d3f85604eafdcd1c5ff9b54f7 Mon Sep 17 00:00:00 2001 From: Aayush Gupta Date: Tue, 28 Oct 2025 00:16:36 +0800 Subject: [PATCH 3/6] Migrate & adapt database tests to Kotlin as well Signed-off-by: Aayush Gupta --- .../newpipe/database/DatabaseMigrationTest.kt | 28 +++-- .../local/history/HistoryRecordManagerTest.kt | 67 ++++++----- .../playlist/LocalPlaylistManagerTest.kt | 2 +- .../playlist/PlaylistLocalItemTest.java | 106 ------------------ .../playlist/PlaylistLocalItemTest.kt | 102 +++++++++++++++++ 5 files changed, 155 insertions(+), 150 deletions(-) delete mode 100644 app/src/test/java/org/schabi/newpipe/database/playlist/PlaylistLocalItemTest.java create mode 100644 app/src/test/java/org/schabi/newpipe/database/playlist/PlaylistLocalItemTest.kt diff --git a/app/src/androidTest/java/org/schabi/newpipe/database/DatabaseMigrationTest.kt b/app/src/androidTest/java/org/schabi/newpipe/database/DatabaseMigrationTest.kt index a34cfece6..4327271f4 100644 --- a/app/src/androidTest/java/org/schabi/newpipe/database/DatabaseMigrationTest.kt +++ b/app/src/androidTest/java/org/schabi/newpipe/database/DatabaseMigrationTest.kt @@ -129,7 +129,7 @@ class DatabaseMigrationTest { ) val migratedDatabaseV3 = getMigratedDatabase() - val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst() + val listFromDB = migratedDatabaseV3.streamDAO().getAll().blockingFirst() // Only expect 2, the one with the null url will be ignored assertEquals(2, listFromDB.size) @@ -217,7 +217,7 @@ class DatabaseMigrationTest { ) val migratedDatabaseV8 = getMigratedDatabase() - val listFromDB = migratedDatabaseV8.searchHistoryDAO().all.blockingFirst() + val listFromDB = migratedDatabaseV8.searchHistoryDAO().getAll().blockingFirst() assertEquals(2, listFromDB.size) assertEquals("abc", listFromDB[0].search) @@ -283,8 +283,8 @@ class DatabaseMigrationTest { ) val migratedDatabaseV9 = getMigratedDatabase() - var localListFromDB = migratedDatabaseV9.playlistDAO().all.blockingFirst() - var remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().all.blockingFirst() + var localListFromDB = migratedDatabaseV9.playlistDAO().getAll().blockingFirst() + var remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().getAll().blockingFirst() assertEquals(1, localListFromDB.size) assertEquals(localUid2, localListFromDB[0].uid) @@ -294,17 +294,27 @@ class DatabaseMigrationTest { assertEquals(-1, remoteListFromDB[0].displayIndex) val localUid3 = migratedDatabaseV9.playlistDAO().insert( - PlaylistEntity(DEFAULT_NAME + "3", false, -1, -1) + PlaylistEntity( + name = "${DEFAULT_NAME}3", + isThumbnailPermanent = false, + thumbnailStreamId = -1, + displayIndex = -1 + ) ) val remoteUid3 = migratedDatabaseV9.playlistRemoteDAO().insert( PlaylistRemoteEntity( - DEFAULT_THIRD_SERVICE_ID, DEFAULT_NAME, DEFAULT_THIRD_URL, - DEFAULT_THUMBNAIL, DEFAULT_UPLOADER_NAME, -1, 10 + serviceId = DEFAULT_THIRD_SERVICE_ID, + orderingName = DEFAULT_NAME, + url = DEFAULT_THIRD_URL, + thumbnailUrl = DEFAULT_THUMBNAIL, + uploader = DEFAULT_UPLOADER_NAME, + displayIndex = -1, + streamCount = 10 ) ) - localListFromDB = migratedDatabaseV9.playlistDAO().all.blockingFirst() - remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().all.blockingFirst() + localListFromDB = migratedDatabaseV9.playlistDAO().getAll().blockingFirst() + remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().getAll().blockingFirst() assertEquals(2, localListFromDB.size) assertEquals(localUid3, localListFromDB[1].uid) assertEquals(-1, localListFromDB[1].displayIndex) diff --git a/app/src/androidTest/java/org/schabi/newpipe/local/history/HistoryRecordManagerTest.kt b/app/src/androidTest/java/org/schabi/newpipe/local/history/HistoryRecordManagerTest.kt index 24be0f868..0de9dd268 100644 --- a/app/src/androidTest/java/org/schabi/newpipe/local/history/HistoryRecordManagerTest.kt +++ b/app/src/androidTest/java/org/schabi/newpipe/local/history/HistoryRecordManagerTest.kt @@ -41,7 +41,7 @@ class HistoryRecordManagerTest { // For some reason the Flowable returned by getAll() never completes, so we can't assert // that the number of Lists it returns is exactly 1, we can only check if the first List is // correct. Why on earth has a Flowable been used instead of a Single for getAll()?!? - val entities = database.searchHistoryDAO().all.blockingFirst() + val entities = database.searchHistoryDAO().getAll().blockingFirst() assertThat(entities).hasSize(1) assertThat(entities[0].id).isEqualTo(1) assertThat(entities[0].serviceId).isEqualTo(0) @@ -51,50 +51,50 @@ class HistoryRecordManagerTest { @Test fun deleteSearchHistory() { val entries = listOf( - SearchHistoryEntry(time.minusSeconds(1), 0, "A"), - SearchHistoryEntry(time.minusSeconds(2), 2, "A"), - SearchHistoryEntry(time.minusSeconds(3), 1, "B"), - SearchHistoryEntry(time.minusSeconds(4), 0, "B"), + SearchHistoryEntry(creationDate = time.minusSeconds(1), serviceId = 0, search = "A"), + SearchHistoryEntry(creationDate = time.minusSeconds(2), serviceId = 2, search = "A"), + SearchHistoryEntry(creationDate = time.minusSeconds(3), serviceId = 1, search = "B"), + SearchHistoryEntry(creationDate = time.minusSeconds(4), serviceId = 0, search = "B"), ) // make sure all 4 were inserted database.searchHistoryDAO().insertAll(entries) - assertThat(database.searchHistoryDAO().all.blockingFirst()).hasSameSizeAs(entries) + assertThat(database.searchHistoryDAO().getAll().blockingFirst()).hasSameSizeAs(entries) // try to delete only "A" entries, "B" entries should be untouched manager.deleteSearchHistory("A").test().await().assertValue(2) - val entities = database.searchHistoryDAO().all.blockingFirst() + val entities = database.searchHistoryDAO().getAll().blockingFirst() assertThat(entities).hasSize(2) assertThat(entities).usingElementComparator { o1, o2 -> if (o1.hasEqualValues(o2)) 0 else 1 } .containsExactly(*entries.subList(2, 4).toTypedArray()) // assert that nothing happens if we delete a search query that does exist in the db manager.deleteSearchHistory("A").test().await().assertValue(0) - val entities2 = database.searchHistoryDAO().all.blockingFirst() + val entities2 = database.searchHistoryDAO().getAll().blockingFirst() assertThat(entities2).hasSize(2) assertThat(entities2).usingElementComparator { o1, o2 -> if (o1.hasEqualValues(o2)) 0 else 1 } .containsExactly(*entries.subList(2, 4).toTypedArray()) // delete all remaining entries manager.deleteSearchHistory("B").test().await().assertValue(2) - assertThat(database.searchHistoryDAO().all.blockingFirst()).isEmpty() + assertThat(database.searchHistoryDAO().getAll().blockingFirst()).isEmpty() } @Test fun deleteCompleteSearchHistory() { val entries = listOf( - SearchHistoryEntry(time.minusSeconds(1), 1, "A"), - SearchHistoryEntry(time.minusSeconds(2), 2, "B"), - SearchHistoryEntry(time.minusSeconds(3), 0, "C"), + SearchHistoryEntry(creationDate = time.minusSeconds(1), serviceId = 1, search = "A"), + SearchHistoryEntry(creationDate = time.minusSeconds(2), serviceId = 2, search = "B"), + SearchHistoryEntry(creationDate = time.minusSeconds(3), serviceId = 0, search = "C"), ) // make sure all 3 were inserted database.searchHistoryDAO().insertAll(entries) - assertThat(database.searchHistoryDAO().all.blockingFirst()).hasSameSizeAs(entries) + assertThat(database.searchHistoryDAO().getAll().blockingFirst()).hasSameSizeAs(entries) // should remove everything manager.deleteCompleteSearchHistory().test().await().assertValue(entries.size) - assertThat(database.searchHistoryDAO().all.blockingFirst()).isEmpty() + assertThat(database.searchHistoryDAO().getAll().blockingFirst()).isEmpty() } private fun insertShuffledRelatedSearches(relatedSearches: Collection) { @@ -107,7 +107,7 @@ class HistoryRecordManagerTest { // make sure all entries were inserted assertEquals( relatedSearches.size, - database.searchHistoryDAO().all.blockingFirst().size + database.searchHistoryDAO().getAll().blockingFirst().size ) } @@ -127,19 +127,18 @@ class HistoryRecordManagerTest { @Test fun getRelatedSearches_emptyQuery_manyDuplicates() { - insertShuffledRelatedSearches( - listOf( - SearchHistoryEntry(time.minusSeconds(9), 3, "A"), - SearchHistoryEntry(time.minusSeconds(8), 3, "AB"), - SearchHistoryEntry(time.minusSeconds(7), 3, "A"), - SearchHistoryEntry(time.minusSeconds(6), 3, "A"), - SearchHistoryEntry(time.minusSeconds(5), 3, "BA"), - SearchHistoryEntry(time.minusSeconds(4), 3, "A"), - SearchHistoryEntry(time.minusSeconds(3), 3, "A"), - SearchHistoryEntry(time.minusSeconds(2), 0, "A"), - SearchHistoryEntry(time.minusSeconds(1), 2, "AA"), - ) + val relatedSearches = listOf( + SearchHistoryEntry(creationDate = time.minusSeconds(9), serviceId = 3, search = "A"), + SearchHistoryEntry(creationDate = time.minusSeconds(8), serviceId = 3, search = "AB"), + SearchHistoryEntry(creationDate = time.minusSeconds(7), serviceId = 3, search = "A"), + SearchHistoryEntry(creationDate = time.minusSeconds(6), serviceId = 3, search = "A"), + SearchHistoryEntry(creationDate = time.minusSeconds(5), serviceId = 3, search = "BA"), + SearchHistoryEntry(creationDate = time.minusSeconds(4), serviceId = 3, search = "A"), + SearchHistoryEntry(creationDate = time.minusSeconds(3), serviceId = 3, search = "A"), + SearchHistoryEntry(creationDate = time.minusSeconds(2), serviceId = 0, search = "A"), + SearchHistoryEntry(creationDate = time.minusSeconds(1), serviceId = 2, search = "AA"), ) + insertShuffledRelatedSearches(relatedSearches) val searches = manager.getRelatedSearches("", 9, 3).blockingFirst() assertThat(searches).containsExactly("AA", "A", "BA") @@ -166,13 +165,13 @@ class HistoryRecordManagerTest { private val time = OffsetDateTime.of(LocalDateTime.of(2000, 1, 1, 1, 1), ZoneOffset.UTC) private val RELATED_SEARCHES_ENTRIES = listOf( - SearchHistoryEntry(time.minusSeconds(7), 2, "AC"), - SearchHistoryEntry(time.minusSeconds(6), 0, "ABC"), - SearchHistoryEntry(time.minusSeconds(5), 1, "BA"), - SearchHistoryEntry(time.minusSeconds(4), 3, "A"), - SearchHistoryEntry(time.minusSeconds(2), 0, "B"), - SearchHistoryEntry(time.minusSeconds(3), 2, "AA"), - SearchHistoryEntry(time.minusSeconds(1), 1, "A"), + SearchHistoryEntry(creationDate = time.minusSeconds(7), serviceId = 2, search = "AC"), + SearchHistoryEntry(creationDate = time.minusSeconds(6), serviceId = 0, search = "ABC"), + SearchHistoryEntry(creationDate = time.minusSeconds(5), serviceId = 1, search = "BA"), + SearchHistoryEntry(creationDate = time.minusSeconds(4), serviceId = 3, search = "A"), + SearchHistoryEntry(creationDate = time.minusSeconds(2), serviceId = 0, search = "B"), + SearchHistoryEntry(creationDate = time.minusSeconds(3), serviceId = 2, search = "AA"), + SearchHistoryEntry(creationDate = time.minusSeconds(1), serviceId = 1, search = "A"), ) } } diff --git a/app/src/androidTest/java/org/schabi/newpipe/local/playlist/LocalPlaylistManagerTest.kt b/app/src/androidTest/java/org/schabi/newpipe/local/playlist/LocalPlaylistManagerTest.kt index c392d8d3d..ce3aeb84a 100644 --- a/app/src/androidTest/java/org/schabi/newpipe/local/playlist/LocalPlaylistManagerTest.kt +++ b/app/src/androidTest/java/org/schabi/newpipe/local/playlist/LocalPlaylistManagerTest.kt @@ -72,6 +72,6 @@ class LocalPlaylistManagerTest { val result = manager.createPlaylist("name", listOf(stream, upserted)) result.test().await().assertComplete() - database.streamDAO().all.test().awaitCount(1).assertValue(listOf(stream, upserted)) + database.streamDAO().getAll().test().awaitCount(1).assertValue(listOf(stream, upserted)) } } diff --git a/app/src/test/java/org/schabi/newpipe/database/playlist/PlaylistLocalItemTest.java b/app/src/test/java/org/schabi/newpipe/database/playlist/PlaylistLocalItemTest.java deleted file mode 100644 index 847c54aa8..000000000 --- a/app/src/test/java/org/schabi/newpipe/database/playlist/PlaylistLocalItemTest.java +++ /dev/null @@ -1,106 +0,0 @@ -package org.schabi.newpipe.database.playlist; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -import org.junit.Test; -import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; -import org.schabi.newpipe.local.bookmark.MergedPlaylistManager; - -import java.util.ArrayList; -import java.util.List; - -public class PlaylistLocalItemTest { - @Test - public void emptyPlaylists() { - final List localPlaylists = new ArrayList<>(); - final List remotePlaylists = new ArrayList<>(); - final List mergedPlaylists = - MergedPlaylistManager.merge(localPlaylists, remotePlaylists); - - assertEquals(0, mergedPlaylists.size()); - } - - @Test - public void onlyLocalPlaylists() { - final List localPlaylists = new ArrayList<>(); - final List remotePlaylists = new ArrayList<>(); - localPlaylists.add(new PlaylistMetadataEntry(1, "name1", "", false, -1, 0, 1)); - localPlaylists.add(new PlaylistMetadataEntry(2, "name2", "", false, -1, 1, 1)); - localPlaylists.add(new PlaylistMetadataEntry(3, "name3", "", false, -1, 3, 1)); - final List mergedPlaylists = - MergedPlaylistManager.merge(localPlaylists, remotePlaylists); - - assertEquals(3, mergedPlaylists.size()); - assertEquals(0, mergedPlaylists.get(0).getDisplayIndex()); - assertEquals(1, mergedPlaylists.get(1).getDisplayIndex()); - assertEquals(3, mergedPlaylists.get(2).getDisplayIndex()); - } - - @Test - public void onlyRemotePlaylists() { - final List localPlaylists = new ArrayList<>(); - final List remotePlaylists = new ArrayList<>(); - remotePlaylists.add(new PlaylistRemoteEntity( - 1, "name1", "url1", "", "", 1, 1L)); - remotePlaylists.add(new PlaylistRemoteEntity( - 2, "name2", "url2", "", "", 2, 1L)); - remotePlaylists.add(new PlaylistRemoteEntity( - 3, "name3", "url3", "", "", 4, 1L)); - final List mergedPlaylists = - MergedPlaylistManager.merge(localPlaylists, remotePlaylists); - - assertEquals(3, mergedPlaylists.size()); - assertEquals(1, mergedPlaylists.get(0).getDisplayIndex()); - assertEquals(2, mergedPlaylists.get(1).getDisplayIndex()); - assertEquals(4, mergedPlaylists.get(2).getDisplayIndex()); - } - - @Test - public void sameIndexWithDifferentName() { - final List localPlaylists = new ArrayList<>(); - final List remotePlaylists = new ArrayList<>(); - localPlaylists.add(new PlaylistMetadataEntry(1, "name1", "", false, -1, 0, 1)); - localPlaylists.add(new PlaylistMetadataEntry(2, "name2", "", false, -1, 1, 1)); - remotePlaylists.add(new PlaylistRemoteEntity( - 1, "name3", "url1", "", "", 0, 1L)); - remotePlaylists.add(new PlaylistRemoteEntity( - 2, "name4", "url2", "", "", 1, 1L)); - final List mergedPlaylists = - MergedPlaylistManager.merge(localPlaylists, remotePlaylists); - - assertEquals(4, mergedPlaylists.size()); - assertTrue(mergedPlaylists.get(0) instanceof PlaylistMetadataEntry); - assertEquals("name1", ((PlaylistMetadataEntry) mergedPlaylists.get(0)).name); - assertTrue(mergedPlaylists.get(1) instanceof PlaylistRemoteEntity); - assertEquals("name3", ((PlaylistRemoteEntity) mergedPlaylists.get(1)).getName()); - assertTrue(mergedPlaylists.get(2) instanceof PlaylistMetadataEntry); - assertEquals("name2", ((PlaylistMetadataEntry) mergedPlaylists.get(2)).name); - assertTrue(mergedPlaylists.get(3) instanceof PlaylistRemoteEntity); - assertEquals("name4", ((PlaylistRemoteEntity) mergedPlaylists.get(3)).getName()); - } - - @Test - public void sameNameWithDifferentIndex() { - final List localPlaylists = new ArrayList<>(); - final List remotePlaylists = new ArrayList<>(); - localPlaylists.add(new PlaylistMetadataEntry(1, "name1", "", false, -1, 1, 1)); - localPlaylists.add(new PlaylistMetadataEntry(2, "name2", "", false, -1, 3, 1)); - remotePlaylists.add(new PlaylistRemoteEntity( - 1, "name1", "url1", "", "", 0, 1L)); - remotePlaylists.add(new PlaylistRemoteEntity( - 2, "name2", "url2", "", "", 2, 1L)); - final List mergedPlaylists = - MergedPlaylistManager.merge(localPlaylists, remotePlaylists); - - assertEquals(4, mergedPlaylists.size()); - assertTrue(mergedPlaylists.get(0) instanceof PlaylistRemoteEntity); - assertEquals("name1", ((PlaylistRemoteEntity) mergedPlaylists.get(0)).getName()); - assertTrue(mergedPlaylists.get(1) instanceof PlaylistMetadataEntry); - assertEquals("name1", ((PlaylistMetadataEntry) mergedPlaylists.get(1)).name); - assertTrue(mergedPlaylists.get(2) instanceof PlaylistRemoteEntity); - assertEquals("name2", ((PlaylistRemoteEntity) mergedPlaylists.get(2)).getName()); - assertTrue(mergedPlaylists.get(3) instanceof PlaylistMetadataEntry); - assertEquals("name2", ((PlaylistMetadataEntry) mergedPlaylists.get(3)).name); - } -} diff --git a/app/src/test/java/org/schabi/newpipe/database/playlist/PlaylistLocalItemTest.kt b/app/src/test/java/org/schabi/newpipe/database/playlist/PlaylistLocalItemTest.kt new file mode 100644 index 000000000..ace51e7e2 --- /dev/null +++ b/app/src/test/java/org/schabi/newpipe/database/playlist/PlaylistLocalItemTest.kt @@ -0,0 +1,102 @@ +/* + * SPDX-FileCopyrightText: 2022-2024 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database.playlist + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity +import org.schabi.newpipe.local.bookmark.MergedPlaylistManager + +class PlaylistLocalItemTest { + + @Test + fun emptyPlaylists() { + val localPlaylists = listOf() + val remotePlaylists = listOf() + val mergedPlaylists = MergedPlaylistManager.merge(localPlaylists, remotePlaylists) + assertEquals(0, mergedPlaylists.size) + } + + @Test + fun onlyLocalPlaylists() { + val localPlaylists = listOf( + PlaylistMetadataEntry(1, "name1", "", 0, false, -1, 1), + PlaylistMetadataEntry(2, "name2", "", 1, false, -1, 1), + PlaylistMetadataEntry(3, "name3", "", 3, false, -1, 1) + ) + val remotePlaylists = listOf() + val mergedPlaylists = MergedPlaylistManager.merge(localPlaylists, remotePlaylists) + + assertEquals(3, mergedPlaylists.size) + assertEquals(0L, mergedPlaylists[0]!!.displayIndex) + assertEquals(1L, mergedPlaylists[1]!!.displayIndex) + assertEquals(3L, mergedPlaylists[2]!!.displayIndex) + } + + @Test + fun onlyRemotePlaylists() { + val localPlaylists = listOf() + val remotePlaylists = listOf( + PlaylistRemoteEntity(1, 1, "name1", "url1", "", "", 1, 1), + PlaylistRemoteEntity(2, 2, "name2", "url2", "", "", 2, 1), + PlaylistRemoteEntity(3, 3, "name3", "url3", "", "", 4, 1) + ) + val mergedPlaylists = MergedPlaylistManager.merge(localPlaylists, remotePlaylists) + + assertEquals(3, mergedPlaylists.size) + assertEquals(1L, mergedPlaylists[0]!!.displayIndex) + assertEquals(2L, mergedPlaylists[1]!!.displayIndex) + assertEquals(4L, mergedPlaylists[2]!!.displayIndex) + } + + @Test + fun sameIndexWithDifferentName() { + val localPlaylists = listOf( + PlaylistMetadataEntry(1, "name1", "", 0, false, -1, 1), + PlaylistMetadataEntry(2, "name2", "", 1, false, -1, 1) + ) + val remotePlaylists = listOf( + PlaylistRemoteEntity(1, 1, "name3", "url1", "", "", 0, 1), + PlaylistRemoteEntity(2, 2, "name4", "url2", "", "", 1, 1) + ) + val mergedPlaylists = MergedPlaylistManager.merge(localPlaylists, remotePlaylists) + + assertEquals(4, mergedPlaylists.size) + assertTrue(mergedPlaylists[0] is PlaylistMetadataEntry) + assertEquals("name1", (mergedPlaylists[0] as PlaylistMetadataEntry).orderingName) + assertTrue(mergedPlaylists[1] is PlaylistRemoteEntity) + assertEquals("name3", (mergedPlaylists[1] as PlaylistRemoteEntity).orderingName) + assertTrue(mergedPlaylists[2] is PlaylistMetadataEntry) + assertEquals("name2", (mergedPlaylists[2] as PlaylistMetadataEntry).orderingName) + assertTrue(mergedPlaylists[3] is PlaylistRemoteEntity) + assertEquals("name4", (mergedPlaylists[3] as PlaylistRemoteEntity).orderingName) + } + + @Test + fun sameNameWithDifferentIndex() { + val localPlaylists = listOf( + PlaylistMetadataEntry(1, "name1", "", 1, false, -1, 1), + PlaylistMetadataEntry(2, "name2", "", 3, false, -1, 1) + ) + val remotePlaylists = listOf( + PlaylistRemoteEntity(1, 1, "name1", "url1", "", "", 0, 1), + PlaylistRemoteEntity(2, 2, "name2", "url2", "", "", 2, 1) + ) + val mergedPlaylists = MergedPlaylistManager.merge(localPlaylists, remotePlaylists) + + assertEquals(4, mergedPlaylists.size) + assertTrue(mergedPlaylists[0] is PlaylistRemoteEntity) + assertEquals("name1", (mergedPlaylists[0] as PlaylistRemoteEntity).orderingName) + assertTrue(mergedPlaylists[1] is PlaylistMetadataEntry) + assertEquals("name1", (mergedPlaylists[1] as PlaylistMetadataEntry).orderingName) + assertTrue(mergedPlaylists[2] is PlaylistRemoteEntity) + assertEquals("name2", (mergedPlaylists[2] as PlaylistRemoteEntity).orderingName) + assertTrue(mergedPlaylists[3] is PlaylistMetadataEntry) + assertEquals("name2", (mergedPlaylists[3] as PlaylistMetadataEntry).orderingName) + } +} From 7c76791db3976c907e14001e703bb9b6143a17a5 Mon Sep 17 00:00:00 2001 From: Aayush Gupta Date: Tue, 28 Oct 2025 15:10:07 +0800 Subject: [PATCH 4/6] Handle null-safety error in FeedDao The lastUpdated parameter can be null, adjust return types to signal that too Signed-off-by: Aayush Gupta --- .../main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt | 4 ++-- .../java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt | 2 +- .../main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt b/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt index e7ed93497..d756df8b1 100644 --- a/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt +++ b/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt @@ -168,10 +168,10 @@ abstract class FeedDAO { ON fgs.subscription_id = lu.subscription_id AND fgs.group_id = :groupId """ ) - abstract fun oldestSubscriptionUpdate(groupId: Long): Flowable> + abstract fun oldestSubscriptionUpdate(groupId: Long): Flowable> @Query("SELECT MIN(last_updated) FROM feed_last_updated") - abstract fun oldestSubscriptionUpdateFromAll(): Flowable> + abstract fun oldestSubscriptionUpdateFromAll(): Flowable> @Query("SELECT COUNT(*) FROM feed_last_updated WHERE last_updated IS NULL") abstract fun notLoadedCount(): Flowable diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt index ed65d4048..aacc6757e 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt @@ -177,7 +177,7 @@ class FeedDatabaseManager(context: Context) { .observeOn(AndroidSchedulers.mainThread()) } - fun oldestSubscriptionUpdate(groupId: Long): Flowable> { + fun oldestSubscriptionUpdate(groupId: Long): Flowable> { return when (groupId) { FeedGroupEntity.GROUP_ALL_ID -> feedTable.oldestSubscriptionUpdateFromAll() else -> feedTable.oldestSubscriptionUpdate(groupId) diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt index 728570b17..f916db2b5 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt @@ -65,7 +65,7 @@ class FeedViewModel( feedDatabaseManager.oldestSubscriptionUpdate(groupId), Function6 { t1: FeedEventManager.Event, t2: Boolean, t3: Boolean, t4: Boolean, - t5: Long, t6: List -> + t5: Long, t6: List -> return@Function6 CombineResultEventHolder(t1, t2, t3, t4, t5, t6.firstOrNull()) } ) From 97e7272151fe40a6576af8d5b8d0315d05fac1ca Mon Sep 17 00:00:00 2001 From: Aayush Gupta Date: Thu, 6 Nov 2025 15:43:07 +0800 Subject: [PATCH 5/6] Removed badly hacked default playlist thumbnail icon The defaults should be supplied to the image loading software not the database library. This would also break when we shrink resources as that would rename the resources. Signed-off-by: Aayush Gupta --- .../playlist/dao/PlaylistStreamDAO.kt | 32 +++---------------- .../database/playlist/model/PlaylistEntity.kt | 4 --- .../local/dialog/PlaylistAppendDialog.java | 7 ++-- 3 files changed, 8 insertions(+), 35 deletions(-) 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 index 11ad38685..7edb15526 100644 --- 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 @@ -15,7 +15,6 @@ 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 @@ -73,11 +72,7 @@ interface PlaylistStreamDAO : BasicDAO { @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, + (SELECT thumbnail_url FROM streams WHERE streams.uid = thumbnail_stream_id) AS thumbnail_url, COALESCE(COUNT(playlist_id), 0) AS streamCount FROM playlists @@ -88,12 +83,7 @@ interface PlaylistStreamDAO : BasicDAO { ORDER BY display_index """ ) - fun getPlaylistMetadata(defaultThumbnail: String): Flowable> - - // TODO: Remove on migrating classes to Kotlin - fun getPlaylistMetadata(): Flowable> { - return getPlaylistMetadata(DEFAULT_THUMBNAIL) - } + fun getPlaylistMetadata(): Flowable> @RewriteQueriesToDropUnusedColumns @Transaction @@ -118,11 +108,7 @@ interface PlaylistStreamDAO : BasicDAO { @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, + (SELECT thumbnail_url FROM streams WHERE streams.uid = thumbnail_stream_id) AS thumbnail_url, COALESCE(COUNT(playlist_id), 0) AS streamCount, COALESCE(SUM(url = :streamUrl), 0) AS timesStreamIsContained FROM playlists @@ -137,15 +123,5 @@ interface PlaylistStreamDAO : BasicDAO { 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) - } + fun getPlaylistDuplicatesMetadata(streamUrl: String): Flowable> } 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 index a72a1cee5..4ea4eb3a7 100644 --- 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 @@ -10,7 +10,6 @@ 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) @@ -42,9 +41,6 @@ data class PlaylistEntity @JvmOverloads constructor( ) companion object { - @JvmField - val DEFAULT_THUMBNAIL = "drawable://" + R.drawable.placeholder_thumbnail_playlist - const val DEFAULT_THUMBNAIL_ID = -1L const val PLAYLIST_TABLE = "playlists" diff --git a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistAppendDialog.java b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistAppendDialog.java index 00b03513d..48d17d1d8 100644 --- a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistAppendDialog.java +++ b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistAppendDialog.java @@ -1,5 +1,7 @@ package org.schabi.newpipe.local.dialog; +import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.DEFAULT_THUMBNAIL_ID; + import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -14,7 +16,6 @@ import androidx.recyclerview.widget.RecyclerView; import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; -import org.schabi.newpipe.database.playlist.model.PlaylistEntity; import org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry; import org.schabi.newpipe.database.stream.model.StreamEntity; import org.schabi.newpipe.local.LocalItemListAdapter; @@ -160,8 +161,8 @@ public final class PlaylistAppendDialog extends PlaylistDialog { .subscribe(ignored -> { successToast.show(); - if (playlist.getThumbnailUrl() != null - && playlist.getThumbnailUrl().equals(PlaylistEntity.DEFAULT_THUMBNAIL) + if (playlist.getThumbnailStreamId() != null + && playlist.getThumbnailStreamId() == DEFAULT_THUMBNAIL_ID ) { playlistDisposables.add(manager .changePlaylistThumbnail(playlist.getUid(), streams.get(0).getUid(), From 4826e5b3c5ebf52b80f4970ba73db7d9d1f3d771 Mon Sep 17 00:00:00 2001 From: Aayush Gupta Date: Thu, 6 Nov 2025 16:08:40 +0800 Subject: [PATCH 6/6] Add missing annotations for columnInfo in PlaylistDuplicatesEntry Fixes [ksp] app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.kt:140: The columns returned by the query does not have the fields [thumbnailUrl,isThumbnailPermanent,thumbnailStreamId,displayIndex,orderingName] in org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry even though they are annotated as non-null or primitive. Columns returned by the query: [uid,streamCount,timesStreamIsContained] Signed-off-by: Aayush Gupta --- .../database/playlist/PlaylistDuplicatesEntry.kt | 14 ++++++++++++++ .../database/playlist/dao/PlaylistStreamDAO.kt | 1 - 2 files changed, 14 insertions(+), 1 deletion(-) 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 index f0447df6e..84972a89e 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistDuplicatesEntry.kt +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistDuplicatesEntry.kt @@ -7,6 +7,7 @@ package org.schabi.newpipe.database.playlist import androidx.room.ColumnInfo +import org.schabi.newpipe.database.playlist.model.PlaylistEntity /** * This class adds a field to [PlaylistMetadataEntry] that contains an integer representing @@ -15,12 +16,25 @@ import androidx.room.ColumnInfo * @see org.schabi.newpipe.local.playlist.LocalPlaylistManager.getPlaylistDuplicates */ data class PlaylistDuplicatesEntry( + @ColumnInfo(name = PlaylistEntity.PLAYLIST_ID) override val uid: Long, + + @ColumnInfo(name = PlaylistEntity.PLAYLIST_THUMBNAIL_URL) override val thumbnailUrl: String?, + + @ColumnInfo(name = PlaylistEntity.PLAYLIST_THUMBNAIL_PERMANENT) override val isThumbnailPermanent: Boolean?, + + @ColumnInfo(name = PlaylistEntity.PLAYLIST_THUMBNAIL_STREAM_ID) override val thumbnailStreamId: Long?, + + @ColumnInfo(name = PlaylistEntity.PLAYLIST_DISPLAY_INDEX) override var displayIndex: Long?, + + @ColumnInfo(name = PLAYLIST_STREAM_COUNT) override val streamCount: Long, + + @ColumnInfo(name = PlaylistEntity.PLAYLIST_NAME) override val orderingName: String?, @ColumnInfo(name = PLAYLIST_TIMES_STREAM_IS_CONTAINED) 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 index 7edb15526..8bf26d754 100644 --- 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 @@ -103,7 +103,6 @@ interface PlaylistStreamDAO : BasicDAO { ) fun getStreamsWithoutDuplicates(playlistId: Long): Flowable> - @RewriteQueriesToDropUnusedColumns @Transaction @Query( """