mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2025-11-13 21:47:11 +00:00
Merge pull request #12746 from TeamNewPipe/kspMigration
Migrate from KAPT to KSP
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.jetbrains.kotlin.android)
|
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.jetbrains.kotlin.parcelize)
|
||||||
alias(libs.plugins.sonarqube)
|
alias(libs.plugins.sonarqube)
|
||||||
checkstyle
|
checkstyle
|
||||||
@@ -40,12 +40,6 @@ android {
|
|||||||
System.getProperty("versionNameSuffix")?.let { versionNameSuffix = it }
|
System.getProperty("versionNameSuffix")?.let { versionNameSuffix = it }
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
javaCompileOptions {
|
|
||||||
annotationProcessorOptions {
|
|
||||||
arguments["room.schemaLocation"] = "$projectDir/schemas"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
@@ -124,6 +118,11 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ksp {
|
||||||
|
arg("room.schemaLocation", "$projectDir/schemas")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Custom dependency configuration for ktlint
|
// Custom dependency configuration for ktlint
|
||||||
val ktlint by configurations.creating
|
val ktlint by configurations.creating
|
||||||
|
|
||||||
@@ -218,7 +217,7 @@ dependencies {
|
|||||||
implementation(libs.androidx.recyclerview)
|
implementation(libs.androidx.recyclerview)
|
||||||
implementation(libs.androidx.room.runtime)
|
implementation(libs.androidx.room.runtime)
|
||||||
implementation(libs.androidx.room.rxjava3)
|
implementation(libs.androidx.room.rxjava3)
|
||||||
kapt(libs.androidx.room.compiler)
|
ksp(libs.androidx.room.compiler)
|
||||||
implementation(libs.androidx.swiperefreshlayout)
|
implementation(libs.androidx.swiperefreshlayout)
|
||||||
implementation(libs.androidx.viewpager2)
|
implementation(libs.androidx.viewpager2)
|
||||||
implementation(libs.androidx.work.runtime)
|
implementation(libs.androidx.work.runtime)
|
||||||
@@ -229,7 +228,7 @@ dependencies {
|
|||||||
/** Third-party libraries **/
|
/** Third-party libraries **/
|
||||||
implementation(libs.livefront.bridge)
|
implementation(libs.livefront.bridge)
|
||||||
implementation(libs.evernote.statesaver.core)
|
implementation(libs.evernote.statesaver.core)
|
||||||
kapt(libs.evernote.statesaver.compiler)
|
ksp(libs.evernote.statesaver.compiler)
|
||||||
|
|
||||||
// HTML parser
|
// HTML parser
|
||||||
implementation(libs.jsoup)
|
implementation(libs.jsoup)
|
||||||
@@ -249,7 +248,7 @@ dependencies {
|
|||||||
|
|
||||||
// Metadata generator for service descriptors
|
// Metadata generator for service descriptors
|
||||||
compileOnly(libs.google.autoservice.annotations)
|
compileOnly(libs.google.autoservice.annotations)
|
||||||
kapt(libs.google.autoservice.compiler)
|
ksp(libs.google.autoservice.compiler)
|
||||||
|
|
||||||
// Manager for complex RecyclerView layouts
|
// Manager for complex RecyclerView layouts
|
||||||
implementation(libs.lisawray.groupie.core)
|
implementation(libs.lisawray.groupie.core)
|
||||||
|
|||||||
@@ -458,7 +458,7 @@
|
|||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldPath": "name",
|
"fieldPath": "orderingName",
|
||||||
"columnName": "name",
|
"columnName": "name",
|
||||||
"affinity": "TEXT",
|
"affinity": "TEXT",
|
||||||
"notNull": false
|
"notNull": false
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ class DatabaseMigrationTest {
|
|||||||
)
|
)
|
||||||
|
|
||||||
val migratedDatabaseV3 = getMigratedDatabase()
|
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
|
// Only expect 2, the one with the null url will be ignored
|
||||||
assertEquals(2, listFromDB.size)
|
assertEquals(2, listFromDB.size)
|
||||||
@@ -217,7 +217,7 @@ class DatabaseMigrationTest {
|
|||||||
)
|
)
|
||||||
|
|
||||||
val migratedDatabaseV8 = getMigratedDatabase()
|
val migratedDatabaseV8 = getMigratedDatabase()
|
||||||
val listFromDB = migratedDatabaseV8.searchHistoryDAO().all.blockingFirst()
|
val listFromDB = migratedDatabaseV8.searchHistoryDAO().getAll().blockingFirst()
|
||||||
|
|
||||||
assertEquals(2, listFromDB.size)
|
assertEquals(2, listFromDB.size)
|
||||||
assertEquals("abc", listFromDB[0].search)
|
assertEquals("abc", listFromDB[0].search)
|
||||||
@@ -283,8 +283,8 @@ class DatabaseMigrationTest {
|
|||||||
)
|
)
|
||||||
|
|
||||||
val migratedDatabaseV9 = getMigratedDatabase()
|
val migratedDatabaseV9 = getMigratedDatabase()
|
||||||
var localListFromDB = migratedDatabaseV9.playlistDAO().all.blockingFirst()
|
var localListFromDB = migratedDatabaseV9.playlistDAO().getAll().blockingFirst()
|
||||||
var remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().all.blockingFirst()
|
var remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().getAll().blockingFirst()
|
||||||
|
|
||||||
assertEquals(1, localListFromDB.size)
|
assertEquals(1, localListFromDB.size)
|
||||||
assertEquals(localUid2, localListFromDB[0].uid)
|
assertEquals(localUid2, localListFromDB[0].uid)
|
||||||
@@ -294,17 +294,27 @@ class DatabaseMigrationTest {
|
|||||||
assertEquals(-1, remoteListFromDB[0].displayIndex)
|
assertEquals(-1, remoteListFromDB[0].displayIndex)
|
||||||
|
|
||||||
val localUid3 = migratedDatabaseV9.playlistDAO().insert(
|
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(
|
val remoteUid3 = migratedDatabaseV9.playlistRemoteDAO().insert(
|
||||||
PlaylistRemoteEntity(
|
PlaylistRemoteEntity(
|
||||||
DEFAULT_THIRD_SERVICE_ID, DEFAULT_NAME, DEFAULT_THIRD_URL,
|
serviceId = DEFAULT_THIRD_SERVICE_ID,
|
||||||
DEFAULT_THUMBNAIL, DEFAULT_UPLOADER_NAME, -1, 10
|
orderingName = DEFAULT_NAME,
|
||||||
|
url = DEFAULT_THIRD_URL,
|
||||||
|
thumbnailUrl = DEFAULT_THUMBNAIL,
|
||||||
|
uploader = DEFAULT_UPLOADER_NAME,
|
||||||
|
displayIndex = -1,
|
||||||
|
streamCount = 10
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
localListFromDB = migratedDatabaseV9.playlistDAO().all.blockingFirst()
|
localListFromDB = migratedDatabaseV9.playlistDAO().getAll().blockingFirst()
|
||||||
remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().all.blockingFirst()
|
remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().getAll().blockingFirst()
|
||||||
assertEquals(2, localListFromDB.size)
|
assertEquals(2, localListFromDB.size)
|
||||||
assertEquals(localUid3, localListFromDB[1].uid)
|
assertEquals(localUid3, localListFromDB[1].uid)
|
||||||
assertEquals(-1, localListFromDB[1].displayIndex)
|
assertEquals(-1, localListFromDB[1].displayIndex)
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class HistoryRecordManagerTest {
|
|||||||
// For some reason the Flowable returned by getAll() never completes, so we can't assert
|
// 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
|
// 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()?!?
|
// 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).hasSize(1)
|
||||||
assertThat(entities[0].id).isEqualTo(1)
|
assertThat(entities[0].id).isEqualTo(1)
|
||||||
assertThat(entities[0].serviceId).isEqualTo(0)
|
assertThat(entities[0].serviceId).isEqualTo(0)
|
||||||
@@ -51,50 +51,50 @@ class HistoryRecordManagerTest {
|
|||||||
@Test
|
@Test
|
||||||
fun deleteSearchHistory() {
|
fun deleteSearchHistory() {
|
||||||
val entries = listOf(
|
val entries = listOf(
|
||||||
SearchHistoryEntry(time.minusSeconds(1), 0, "A"),
|
SearchHistoryEntry(creationDate = time.minusSeconds(1), serviceId = 0, search = "A"),
|
||||||
SearchHistoryEntry(time.minusSeconds(2), 2, "A"),
|
SearchHistoryEntry(creationDate = time.minusSeconds(2), serviceId = 2, search = "A"),
|
||||||
SearchHistoryEntry(time.minusSeconds(3), 1, "B"),
|
SearchHistoryEntry(creationDate = time.minusSeconds(3), serviceId = 1, search = "B"),
|
||||||
SearchHistoryEntry(time.minusSeconds(4), 0, "B"),
|
SearchHistoryEntry(creationDate = time.minusSeconds(4), serviceId = 0, search = "B"),
|
||||||
)
|
)
|
||||||
|
|
||||||
// make sure all 4 were inserted
|
// make sure all 4 were inserted
|
||||||
database.searchHistoryDAO().insertAll(entries)
|
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
|
// try to delete only "A" entries, "B" entries should be untouched
|
||||||
manager.deleteSearchHistory("A").test().await().assertValue(2)
|
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).hasSize(2)
|
||||||
assertThat(entities).usingElementComparator { o1, o2 -> if (o1.hasEqualValues(o2)) 0 else 1 }
|
assertThat(entities).usingElementComparator { o1, o2 -> if (o1.hasEqualValues(o2)) 0 else 1 }
|
||||||
.containsExactly(*entries.subList(2, 4).toTypedArray())
|
.containsExactly(*entries.subList(2, 4).toTypedArray())
|
||||||
|
|
||||||
// assert that nothing happens if we delete a search query that does exist in the db
|
// assert that nothing happens if we delete a search query that does exist in the db
|
||||||
manager.deleteSearchHistory("A").test().await().assertValue(0)
|
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).hasSize(2)
|
||||||
assertThat(entities2).usingElementComparator { o1, o2 -> if (o1.hasEqualValues(o2)) 0 else 1 }
|
assertThat(entities2).usingElementComparator { o1, o2 -> if (o1.hasEqualValues(o2)) 0 else 1 }
|
||||||
.containsExactly(*entries.subList(2, 4).toTypedArray())
|
.containsExactly(*entries.subList(2, 4).toTypedArray())
|
||||||
|
|
||||||
// delete all remaining entries
|
// delete all remaining entries
|
||||||
manager.deleteSearchHistory("B").test().await().assertValue(2)
|
manager.deleteSearchHistory("B").test().await().assertValue(2)
|
||||||
assertThat(database.searchHistoryDAO().all.blockingFirst()).isEmpty()
|
assertThat(database.searchHistoryDAO().getAll().blockingFirst()).isEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun deleteCompleteSearchHistory() {
|
fun deleteCompleteSearchHistory() {
|
||||||
val entries = listOf(
|
val entries = listOf(
|
||||||
SearchHistoryEntry(time.minusSeconds(1), 1, "A"),
|
SearchHistoryEntry(creationDate = time.minusSeconds(1), serviceId = 1, search = "A"),
|
||||||
SearchHistoryEntry(time.minusSeconds(2), 2, "B"),
|
SearchHistoryEntry(creationDate = time.minusSeconds(2), serviceId = 2, search = "B"),
|
||||||
SearchHistoryEntry(time.minusSeconds(3), 0, "C"),
|
SearchHistoryEntry(creationDate = time.minusSeconds(3), serviceId = 0, search = "C"),
|
||||||
)
|
)
|
||||||
|
|
||||||
// make sure all 3 were inserted
|
// make sure all 3 were inserted
|
||||||
database.searchHistoryDAO().insertAll(entries)
|
database.searchHistoryDAO().insertAll(entries)
|
||||||
assertThat(database.searchHistoryDAO().all.blockingFirst()).hasSameSizeAs(entries)
|
assertThat(database.searchHistoryDAO().getAll().blockingFirst()).hasSameSizeAs(entries)
|
||||||
|
|
||||||
// should remove everything
|
// should remove everything
|
||||||
manager.deleteCompleteSearchHistory().test().await().assertValue(entries.size)
|
manager.deleteCompleteSearchHistory().test().await().assertValue(entries.size)
|
||||||
assertThat(database.searchHistoryDAO().all.blockingFirst()).isEmpty()
|
assertThat(database.searchHistoryDAO().getAll().blockingFirst()).isEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun insertShuffledRelatedSearches(relatedSearches: Collection<SearchHistoryEntry>) {
|
private fun insertShuffledRelatedSearches(relatedSearches: Collection<SearchHistoryEntry>) {
|
||||||
@@ -107,7 +107,7 @@ class HistoryRecordManagerTest {
|
|||||||
// make sure all entries were inserted
|
// make sure all entries were inserted
|
||||||
assertEquals(
|
assertEquals(
|
||||||
relatedSearches.size,
|
relatedSearches.size,
|
||||||
database.searchHistoryDAO().all.blockingFirst().size
|
database.searchHistoryDAO().getAll().blockingFirst().size
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,19 +127,18 @@ class HistoryRecordManagerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun getRelatedSearches_emptyQuery_manyDuplicates() {
|
fun getRelatedSearches_emptyQuery_manyDuplicates() {
|
||||||
insertShuffledRelatedSearches(
|
val relatedSearches = listOf(
|
||||||
listOf(
|
SearchHistoryEntry(creationDate = time.minusSeconds(9), serviceId = 3, search = "A"),
|
||||||
SearchHistoryEntry(time.minusSeconds(9), 3, "A"),
|
SearchHistoryEntry(creationDate = time.minusSeconds(8), serviceId = 3, search = "AB"),
|
||||||
SearchHistoryEntry(time.minusSeconds(8), 3, "AB"),
|
SearchHistoryEntry(creationDate = time.minusSeconds(7), serviceId = 3, search = "A"),
|
||||||
SearchHistoryEntry(time.minusSeconds(7), 3, "A"),
|
SearchHistoryEntry(creationDate = time.minusSeconds(6), serviceId = 3, search = "A"),
|
||||||
SearchHistoryEntry(time.minusSeconds(6), 3, "A"),
|
SearchHistoryEntry(creationDate = time.minusSeconds(5), serviceId = 3, search = "BA"),
|
||||||
SearchHistoryEntry(time.minusSeconds(5), 3, "BA"),
|
SearchHistoryEntry(creationDate = time.minusSeconds(4), serviceId = 3, search = "A"),
|
||||||
SearchHistoryEntry(time.minusSeconds(4), 3, "A"),
|
SearchHistoryEntry(creationDate = time.minusSeconds(3), serviceId = 3, search = "A"),
|
||||||
SearchHistoryEntry(time.minusSeconds(3), 3, "A"),
|
SearchHistoryEntry(creationDate = time.minusSeconds(2), serviceId = 0, search = "A"),
|
||||||
SearchHistoryEntry(time.minusSeconds(2), 0, "A"),
|
SearchHistoryEntry(creationDate = time.minusSeconds(1), serviceId = 2, search = "AA"),
|
||||||
SearchHistoryEntry(time.minusSeconds(1), 2, "AA"),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
insertShuffledRelatedSearches(relatedSearches)
|
||||||
|
|
||||||
val searches = manager.getRelatedSearches("", 9, 3).blockingFirst()
|
val searches = manager.getRelatedSearches("", 9, 3).blockingFirst()
|
||||||
assertThat(searches).containsExactly("AA", "A", "BA")
|
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 time = OffsetDateTime.of(LocalDateTime.of(2000, 1, 1, 1, 1), ZoneOffset.UTC)
|
||||||
|
|
||||||
private val RELATED_SEARCHES_ENTRIES = listOf(
|
private val RELATED_SEARCHES_ENTRIES = listOf(
|
||||||
SearchHistoryEntry(time.minusSeconds(7), 2, "AC"),
|
SearchHistoryEntry(creationDate = time.minusSeconds(7), serviceId = 2, search = "AC"),
|
||||||
SearchHistoryEntry(time.minusSeconds(6), 0, "ABC"),
|
SearchHistoryEntry(creationDate = time.minusSeconds(6), serviceId = 0, search = "ABC"),
|
||||||
SearchHistoryEntry(time.minusSeconds(5), 1, "BA"),
|
SearchHistoryEntry(creationDate = time.minusSeconds(5), serviceId = 1, search = "BA"),
|
||||||
SearchHistoryEntry(time.minusSeconds(4), 3, "A"),
|
SearchHistoryEntry(creationDate = time.minusSeconds(4), serviceId = 3, search = "A"),
|
||||||
SearchHistoryEntry(time.minusSeconds(2), 0, "B"),
|
SearchHistoryEntry(creationDate = time.minusSeconds(2), serviceId = 0, search = "B"),
|
||||||
SearchHistoryEntry(time.minusSeconds(3), 2, "AA"),
|
SearchHistoryEntry(creationDate = time.minusSeconds(3), serviceId = 2, search = "AA"),
|
||||||
SearchHistoryEntry(time.minusSeconds(1), 1, "A"),
|
SearchHistoryEntry(creationDate = time.minusSeconds(1), serviceId = 1, search = "A"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,6 @@ class LocalPlaylistManagerTest {
|
|||||||
val result = manager.createPlaylist("name", listOf(stream, upserted))
|
val result = manager.createPlaylist("name", listOf(stream, upserted))
|
||||||
|
|
||||||
result.test().await().assertComplete()
|
result.test().await().assertComplete()
|
||||||
database.streamDAO().all.test().awaitCount(1).assertValue(listOf(stream, upserted))
|
database.streamDAO().getAll().test().awaitCount(1).assertValue(listOf(stream, upserted))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
80
app/src/main/java/org/schabi/newpipe/NewPipeDatabase.kt
Normal file
80
app/src/main/java/org/schabi/newpipe/NewPipeDatabase.kt
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2017-2024 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
68
app/src/main/java/org/schabi/newpipe/database/AppDatabase.kt
Normal file
68
app/src/main/java/org/schabi/newpipe/database/AppDatabase.kt
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2017-2024 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Entity> {
|
|
||||||
/* Inserts */
|
|
||||||
@Insert
|
|
||||||
long insert(Entity entity);
|
|
||||||
|
|
||||||
@Insert
|
|
||||||
List<Long> insertAll(Collection<Entity> entities);
|
|
||||||
|
|
||||||
/* Searches */
|
|
||||||
Flowable<List<Entity>> getAll();
|
|
||||||
|
|
||||||
Flowable<List<Entity>> listByService(int serviceId);
|
|
||||||
|
|
||||||
/* Deletes */
|
|
||||||
@Delete
|
|
||||||
void delete(Entity entity);
|
|
||||||
|
|
||||||
int deleteAll();
|
|
||||||
|
|
||||||
/* Updates */
|
|
||||||
@Update
|
|
||||||
int update(Entity entity);
|
|
||||||
|
|
||||||
@Update
|
|
||||||
void update(Collection<Entity> entities);
|
|
||||||
}
|
|
||||||
42
app/src/main/java/org/schabi/newpipe/database/BasicDAO.kt
Normal file
42
app/src/main/java/org/schabi/newpipe/database/BasicDAO.kt
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2017-2022 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* 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<Entity> {
|
||||||
|
|
||||||
|
/* Inserts */
|
||||||
|
@Insert
|
||||||
|
fun insert(entity: Entity): Long
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
fun insertAll(entities: Collection<Entity>): List<Long>
|
||||||
|
|
||||||
|
/* Searches */
|
||||||
|
fun getAll(): Flowable<List<Entity>>
|
||||||
|
|
||||||
|
fun listByService(serviceId: Int): Flowable<List<Entity>>
|
||||||
|
|
||||||
|
/* Deletes */
|
||||||
|
@Delete
|
||||||
|
fun delete(entity: Entity)
|
||||||
|
|
||||||
|
fun deleteAll(): Int
|
||||||
|
|
||||||
|
/* Updates */
|
||||||
|
@Update
|
||||||
|
fun update(entity: Entity): Int
|
||||||
|
|
||||||
|
@Update
|
||||||
|
fun update(entities: Collection<Entity>)
|
||||||
|
}
|
||||||
@@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
19
app/src/main/java/org/schabi/newpipe/database/LocalItem.kt
Normal file
19
app/src/main/java/org/schabi/newpipe/database/LocalItem.kt
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2018-2020 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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() {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
368
app/src/main/java/org/schabi/newpipe/database/Migrations.kt
Normal file
368
app/src/main/java/org/schabi/newpipe/database/Migrations.kt
Normal file
@@ -0,0 +1,368 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2018-2024 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -168,10 +168,10 @@ abstract class FeedDAO {
|
|||||||
ON fgs.subscription_id = lu.subscription_id AND fgs.group_id = :groupId
|
ON fgs.subscription_id = lu.subscription_id AND fgs.group_id = :groupId
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
abstract fun oldestSubscriptionUpdate(groupId: Long): Flowable<List<OffsetDateTime>>
|
abstract fun oldestSubscriptionUpdate(groupId: Long): Flowable<List<OffsetDateTime?>>
|
||||||
|
|
||||||
@Query("SELECT MIN(last_updated) FROM feed_last_updated")
|
@Query("SELECT MIN(last_updated) FROM feed_last_updated")
|
||||||
abstract fun oldestSubscriptionUpdateFromAll(): Flowable<List<OffsetDateTime>>
|
abstract fun oldestSubscriptionUpdateFromAll(): Flowable<List<OffsetDateTime?>>
|
||||||
|
|
||||||
@Query("SELECT COUNT(*) FROM feed_last_updated WHERE last_updated IS NULL")
|
@Query("SELECT COUNT(*) FROM feed_last_updated WHERE last_updated IS NULL")
|
||||||
abstract fun notLoadedCount(): Flowable<Long>
|
abstract fun notLoadedCount(): Flowable<Long>
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
package org.schabi.newpipe.database.history.dao;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.database.BasicDAO;
|
|
||||||
|
|
||||||
public interface HistoryDAO<T> extends BasicDAO<T> {
|
|
||||||
T getLatestEntry();
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2017 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.database.history.dao
|
||||||
|
|
||||||
|
import org.schabi.newpipe.database.BasicDAO
|
||||||
|
|
||||||
|
interface HistoryDAO<T> : BasicDAO<T> {
|
||||||
|
val latestEntry: T
|
||||||
|
}
|
||||||
@@ -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<SearchHistoryEntry> {
|
|
||||||
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<List<SearchHistoryEntry>> getAll();
|
|
||||||
|
|
||||||
@Query("SELECT " + SEARCH + " FROM " + TABLE_NAME + " GROUP BY " + SEARCH
|
|
||||||
+ ORDER_BY_MAX_CREATION_DATE + " LIMIT :limit")
|
|
||||||
Flowable<List<String>> getUniqueEntries(int limit);
|
|
||||||
|
|
||||||
@Query("SELECT * FROM " + TABLE_NAME
|
|
||||||
+ " WHERE " + SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE)
|
|
||||||
@Override
|
|
||||||
Flowable<List<SearchHistoryEntry>> listByService(int serviceId);
|
|
||||||
|
|
||||||
@Query("SELECT " + SEARCH + " FROM " + TABLE_NAME + " WHERE " + SEARCH + " LIKE :query || '%'"
|
|
||||||
+ " GROUP BY " + SEARCH + ORDER_BY_MAX_CREATION_DATE + " LIMIT :limit")
|
|
||||||
Flowable<List<String>> getSimilarEntries(String query, int limit);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2017-2021 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* 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<SearchHistoryEntry> {
|
||||||
|
|
||||||
|
@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<List<SearchHistoryEntry>>
|
||||||
|
|
||||||
|
@Query("SELECT search FROM search_history GROUP BY search ORDER BY MAX(creation_date) DESC LIMIT :limit")
|
||||||
|
fun getUniqueEntries(limit: Int): Flowable<MutableList<String>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM search_history WHERE service_id = :serviceId ORDER BY creation_date DESC")
|
||||||
|
override fun listByService(serviceId: Int): Flowable<List<SearchHistoryEntry>>
|
||||||
|
|
||||||
|
@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<MutableList<String>>
|
||||||
|
}
|
||||||
@@ -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<StreamHistoryEntity> {
|
|
||||||
@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<List<StreamHistoryEntity>> getAll();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Query("DELETE FROM " + STREAM_HISTORY_TABLE)
|
|
||||||
public abstract int deleteAll();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Flowable<List<StreamHistoryEntity>> 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<List<StreamHistoryEntry>> 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<List<StreamHistoryEntry>> 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<List<StreamStatisticsEntry>> getStatistics();
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2018-2022 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* 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<StreamHistoryEntity> {
|
||||||
|
|
||||||
|
@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<List<StreamHistoryEntity>>
|
||||||
|
|
||||||
|
@Query("DELETE FROM stream_history")
|
||||||
|
abstract override fun deleteAll(): Int
|
||||||
|
|
||||||
|
override fun listByService(serviceId: Int): Flowable<List<StreamHistoryEntity>> {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
@get:Query("SELECT * FROM streams INNER JOIN stream_history ON uid = stream_id ORDER BY access_date DESC")
|
||||||
|
abstract val history: Flowable<MutableList<StreamHistoryEntry>>
|
||||||
|
|
||||||
|
@get:Query("SELECT * FROM streams INNER JOIN stream_history ON uid = stream_id ORDER BY uid ASC")
|
||||||
|
abstract val historySortedById: Flowable<MutableList<StreamHistoryEntry>>
|
||||||
|
|
||||||
|
@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<MutableList<StreamStatisticsEntry>>
|
||||||
|
}
|
||||||
@@ -1,3 +1,9 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2022 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
package org.schabi.newpipe.database.history.model
|
package org.schabi.newpipe.database.history.model
|
||||||
|
|
||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
@@ -11,23 +17,24 @@ import java.time.OffsetDateTime
|
|||||||
tableName = SearchHistoryEntry.TABLE_NAME,
|
tableName = SearchHistoryEntry.TABLE_NAME,
|
||||||
indices = [Index(value = [SearchHistoryEntry.SEARCH])]
|
indices = [Index(value = [SearchHistoryEntry.SEARCH])]
|
||||||
)
|
)
|
||||||
data class SearchHistoryEntry(
|
data class SearchHistoryEntry @JvmOverloads constructor(
|
||||||
@field:ColumnInfo(name = CREATION_DATE) var creationDate: OffsetDateTime?,
|
@ColumnInfo(name = CREATION_DATE)
|
||||||
@field:ColumnInfo(
|
var creationDate: OffsetDateTime?,
|
||||||
name = SERVICE_ID
|
|
||||||
) var serviceId: Int,
|
@ColumnInfo(name = SERVICE_ID)
|
||||||
@field:ColumnInfo(name = SEARCH) var search: String?
|
val serviceId: Int,
|
||||||
) {
|
|
||||||
|
@ColumnInfo(name = SEARCH)
|
||||||
|
val search: String?,
|
||||||
|
|
||||||
@ColumnInfo(name = ID)
|
@ColumnInfo(name = ID)
|
||||||
@PrimaryKey(autoGenerate = true)
|
@PrimaryKey(autoGenerate = true)
|
||||||
var id: Long = 0
|
val id: Long = 0,
|
||||||
|
) {
|
||||||
|
|
||||||
@Ignore
|
@Ignore
|
||||||
fun hasEqualValues(otherEntry: SearchHistoryEntry): Boolean {
|
fun hasEqualValues(otherEntry: SearchHistoryEntry): Boolean {
|
||||||
return (
|
return serviceId == otherEntry.serviceId && search == otherEntry.search
|
||||||
serviceId == otherEntry.serviceId &&
|
|
||||||
search == otherEntry.search
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2018-2022 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023-2024 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
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
|
||||||
|
* 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(
|
||||||
|
@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)
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2018-2025 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* 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?
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2018-2025 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,9 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2020-2023 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
package org.schabi.newpipe.database.playlist
|
package org.schabi.newpipe.database.playlist
|
||||||
|
|
||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
@@ -23,18 +29,21 @@ data class PlaylistStreamEntry(
|
|||||||
val joinIndex: Int
|
val joinIndex: Int
|
||||||
) : LocalItem {
|
) : LocalItem {
|
||||||
|
|
||||||
|
override val localItemType: LocalItem.LocalItemType
|
||||||
|
get() = LocalItem.LocalItemType.PLAYLIST_STREAM_ITEM
|
||||||
|
|
||||||
@Throws(IllegalArgumentException::class)
|
@Throws(IllegalArgumentException::class)
|
||||||
fun toStreamInfoItem(): StreamInfoItem {
|
fun toStreamInfoItem(): StreamInfoItem {
|
||||||
val item = StreamInfoItem(streamEntity.serviceId, streamEntity.url, streamEntity.title, streamEntity.streamType)
|
return StreamInfoItem(
|
||||||
item.duration = streamEntity.duration
|
streamEntity.serviceId,
|
||||||
item.uploaderName = streamEntity.uploader
|
streamEntity.url,
|
||||||
item.uploaderUrl = streamEntity.uploaderUrl
|
streamEntity.title,
|
||||||
item.thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl)
|
streamEntity.streamType
|
||||||
|
).apply {
|
||||||
return item
|
duration = streamEntity.duration
|
||||||
|
uploaderName = streamEntity.uploader
|
||||||
|
uploaderUrl = streamEntity.uploaderUrl
|
||||||
|
thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLocalItemType(): LocalItem.LocalItemType {
|
|
||||||
return LocalItem.LocalItemType.PLAYLIST_STREAM_ITEM
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<PlaylistEntity> {
|
|
||||||
@Override
|
|
||||||
@Query("SELECT * FROM " + PLAYLIST_TABLE)
|
|
||||||
Flowable<List<PlaylistEntity>> getAll();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Query("DELETE FROM " + PLAYLIST_TABLE)
|
|
||||||
int deleteAll();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
default Flowable<List<PlaylistEntity>> listByService(final int serviceId) {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Query("SELECT * FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId")
|
|
||||||
Flowable<List<PlaylistEntity>> getPlaylist(long playlistId);
|
|
||||||
|
|
||||||
@Query("DELETE FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId")
|
|
||||||
int deletePlaylist(long playlistId);
|
|
||||||
|
|
||||||
@Query("SELECT COUNT(*) FROM " + PLAYLIST_TABLE)
|
|
||||||
Flowable<Long> 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2018-2022 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* 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<PlaylistEntity> {
|
||||||
|
|
||||||
|
@Query("SELECT * FROM playlists")
|
||||||
|
override fun getAll(): Flowable<List<PlaylistEntity>>
|
||||||
|
|
||||||
|
@Query("DELETE FROM playlists")
|
||||||
|
override fun deleteAll(): Int
|
||||||
|
|
||||||
|
override fun listByService(serviceId: Int): Flowable<List<PlaylistEntity>> {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query("SELECT * FROM playlists WHERE uid = :playlistId")
|
||||||
|
fun getPlaylist(playlistId: Long): Flowable<MutableList<PlaylistEntity>>
|
||||||
|
|
||||||
|
@Query("DELETE FROM playlists WHERE uid = :playlistId")
|
||||||
|
fun deletePlaylist(playlistId: Long): Int
|
||||||
|
|
||||||
|
@get:Query("SELECT COUNT(*) FROM playlists")
|
||||||
|
val count: Flowable<Long>
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
fun upsertPlaylist(playlist: PlaylistEntity): Long {
|
||||||
|
if (playlist.uid == -1L) {
|
||||||
|
// This situation is probably impossible.
|
||||||
|
return insert(playlist)
|
||||||
|
} else {
|
||||||
|
update(playlist)
|
||||||
|
return playlist.uid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<PlaylistRemoteEntity> {
|
|
||||||
@Override
|
|
||||||
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE)
|
|
||||||
Flowable<List<PlaylistRemoteEntity>> getAll();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Query("DELETE FROM " + REMOTE_PLAYLIST_TABLE)
|
|
||||||
int deleteAll();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE
|
|
||||||
+ " WHERE " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
|
||||||
Flowable<List<PlaylistRemoteEntity>> listByService(int serviceId);
|
|
||||||
|
|
||||||
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE "
|
|
||||||
+ REMOTE_PLAYLIST_ID + " = :playlistId")
|
|
||||||
Flowable<PlaylistRemoteEntity> getPlaylist(long playlistId);
|
|
||||||
|
|
||||||
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE "
|
|
||||||
+ REMOTE_PLAYLIST_URL + " = :url AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
|
||||||
Flowable<List<PlaylistRemoteEntity>> getPlaylist(long serviceId, String url);
|
|
||||||
|
|
||||||
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE
|
|
||||||
+ " ORDER BY " + REMOTE_PLAYLIST_DISPLAY_INDEX)
|
|
||||||
Flowable<List<PlaylistRemoteEntity>> 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);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2018-2025 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* 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<PlaylistRemoteEntity> {
|
||||||
|
|
||||||
|
@Query("SELECT * FROM remote_playlists")
|
||||||
|
override fun getAll(): Flowable<List<PlaylistRemoteEntity>>
|
||||||
|
|
||||||
|
@Query("DELETE FROM remote_playlists")
|
||||||
|
override fun deleteAll(): Int
|
||||||
|
|
||||||
|
@Query("SELECT * FROM remote_playlists WHERE service_id = :serviceId")
|
||||||
|
override fun listByService(serviceId: Int): Flowable<List<PlaylistRemoteEntity>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM remote_playlists WHERE uid = :playlistId")
|
||||||
|
fun getPlaylist(playlistId: Long): Flowable<PlaylistRemoteEntity>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM remote_playlists WHERE url = :url AND uid = :serviceId")
|
||||||
|
fun getPlaylist(serviceId: Long, url: String?): Flowable<MutableList<PlaylistRemoteEntity>>
|
||||||
|
|
||||||
|
@get:Query("SELECT * FROM remote_playlists ORDER BY display_index")
|
||||||
|
val playlists: Flowable<MutableList<PlaylistRemoteEntity>>
|
||||||
|
|
||||||
|
@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
|
||||||
|
}
|
||||||
@@ -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<PlaylistStreamEntity> {
|
|
||||||
@Override
|
|
||||||
@Query("SELECT * FROM " + PLAYLIST_STREAM_JOIN_TABLE)
|
|
||||||
Flowable<List<PlaylistStreamEntity>> getAll();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Query("DELETE FROM " + PLAYLIST_STREAM_JOIN_TABLE)
|
|
||||||
int deleteAll();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
default Flowable<List<PlaylistStreamEntity>> 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<Integer> 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<Long> 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<List<PlaylistStreamEntry>> 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<List<PlaylistMetadataEntry>> 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<List<PlaylistStreamEntry>> 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<List<PlaylistDuplicatesEntry>> getPlaylistDuplicatesMetadata(String streamUrl);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2018-2024 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* 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_ID
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface PlaylistStreamDAO : BasicDAO<PlaylistStreamEntity> {
|
||||||
|
|
||||||
|
@Query("SELECT * FROM playlist_stream_join")
|
||||||
|
override fun getAll(): Flowable<List<PlaylistStreamEntity>>
|
||||||
|
|
||||||
|
@Query("DELETE FROM playlist_stream_join")
|
||||||
|
override fun deleteAll(): Int
|
||||||
|
|
||||||
|
override fun listByService(serviceId: Int): Flowable<List<PlaylistStreamEntity>> {
|
||||||
|
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<Int>
|
||||||
|
|
||||||
|
@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<Long>
|
||||||
|
|
||||||
|
// 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<MutableList<PlaylistStreamEntry>>
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT uid, name, is_thumbnail_permanent, thumbnail_stream_id, display_index,
|
||||||
|
(SELECT thumbnail_url FROM streams WHERE streams.uid = thumbnail_stream_id) 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(): Flowable<MutableList<PlaylistMetadataEntry>>
|
||||||
|
|
||||||
|
@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<MutableList<PlaylistStreamEntry>>
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT playlists.uid, name, is_thumbnail_permanent, thumbnail_stream_id, display_index,
|
||||||
|
(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
|
||||||
|
|
||||||
|
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): Flowable<MutableList<PlaylistDuplicatesEntry>>
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2018-2024 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* 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.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 {
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2018-2025 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2018-2020 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2020-2023 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
package org.schabi.newpipe.database.stream
|
package org.schabi.newpipe.database.stream
|
||||||
|
|
||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Embedded
|
import androidx.room.Embedded
|
||||||
|
import androidx.room.Ignore
|
||||||
import org.schabi.newpipe.database.LocalItem
|
import org.schabi.newpipe.database.LocalItem
|
||||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntity
|
import org.schabi.newpipe.database.history.model.StreamHistoryEntity
|
||||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
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.extractor.stream.StreamInfoItem
|
||||||
import org.schabi.newpipe.util.image.ImageStrategy
|
import org.schabi.newpipe.util.image.ImageStrategy
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
class StreamStatisticsEntry(
|
data class StreamStatisticsEntry(
|
||||||
@Embedded
|
@Embedded
|
||||||
val streamEntity: StreamEntity,
|
val streamEntity: StreamEntity,
|
||||||
|
|
||||||
@@ -26,18 +33,23 @@ class StreamStatisticsEntry(
|
|||||||
@ColumnInfo(name = STREAM_WATCH_COUNT)
|
@ColumnInfo(name = STREAM_WATCH_COUNT)
|
||||||
val watchCount: Long
|
val watchCount: Long
|
||||||
) : LocalItem {
|
) : LocalItem {
|
||||||
|
|
||||||
|
override val localItemType: LocalItem.LocalItemType
|
||||||
|
get() = LocalItem.LocalItemType.STATISTIC_STREAM_ITEM
|
||||||
|
|
||||||
|
@Ignore
|
||||||
fun toStreamInfoItem(): StreamInfoItem {
|
fun toStreamInfoItem(): StreamInfoItem {
|
||||||
val item = StreamInfoItem(streamEntity.serviceId, streamEntity.url, streamEntity.title, streamEntity.streamType)
|
return StreamInfoItem(
|
||||||
item.duration = streamEntity.duration
|
streamEntity.serviceId,
|
||||||
item.uploaderName = streamEntity.uploader
|
streamEntity.url,
|
||||||
item.uploaderUrl = streamEntity.uploaderUrl
|
streamEntity.title,
|
||||||
item.thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl)
|
streamEntity.streamType
|
||||||
|
).apply {
|
||||||
return item
|
duration = streamEntity.duration
|
||||||
|
uploaderName = streamEntity.uploader
|
||||||
|
uploaderUrl = streamEntity.uploaderUrl
|
||||||
|
thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLocalItemType(): LocalItem.LocalItemType {
|
|
||||||
return LocalItem.LocalItemType.STATISTIC_STREAM_ITEM
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -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<StreamStateEntity> {
|
|
||||||
@Override
|
|
||||||
@Query("SELECT * FROM " + STREAM_STATE_TABLE)
|
|
||||||
Flowable<List<StreamStateEntity>> getAll();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Query("DELETE FROM " + STREAM_STATE_TABLE)
|
|
||||||
int deleteAll();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
default Flowable<List<StreamStateEntity>> listByService(final int serviceId) {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Query("SELECT * FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
|
||||||
Flowable<List<StreamStateEntity>> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2018-2021 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* 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<StreamStateEntity> {
|
||||||
|
|
||||||
|
@Query("SELECT * FROM " + StreamStateEntity.STREAM_STATE_TABLE)
|
||||||
|
override fun getAll(): Flowable<List<StreamStateEntity>>
|
||||||
|
|
||||||
|
@Query("DELETE FROM " + StreamStateEntity.STREAM_STATE_TABLE)
|
||||||
|
override fun deleteAll(): Int
|
||||||
|
|
||||||
|
override fun listByService(serviceId: Int): Flowable<List<StreamStateEntity>> {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query("SELECT * FROM " + StreamStateEntity.STREAM_STATE_TABLE + " WHERE " + StreamStateEntity.JOIN_STREAM_ID + " = :streamId")
|
||||||
|
fun getState(streamId: Long): Flowable<MutableList<StreamStateEntity>>
|
||||||
|
|
||||||
|
@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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2018-2023 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -99,7 +99,7 @@ abstract class SubscriptionDAO : BasicDAO<SubscriptionEntity> {
|
|||||||
if (uidFromInsert != -1L) {
|
if (uidFromInsert != -1L) {
|
||||||
entity.uid = uidFromInsert
|
entity.uid = uidFromInsert
|
||||||
} else {
|
} else {
|
||||||
val subscriptionIdFromDb = getSubscriptionIdInternal(entity.serviceId, entity.url)
|
val subscriptionIdFromDb = getSubscriptionIdInternal(entity.serviceId, entity.url!!)
|
||||||
?: throw IllegalStateException("Subscription cannot be null just after insertion.")
|
?: throw IllegalStateException("Subscription cannot be null just after insertion.")
|
||||||
entity.uid = subscriptionIdFromDb
|
entity.uid = subscriptionIdFromDb
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2017-2024 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* 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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -361,10 +361,10 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
final SubscriptionEntity channel = new SubscriptionEntity();
|
final SubscriptionEntity channel = new SubscriptionEntity();
|
||||||
channel.setServiceId(info.getServiceId());
|
channel.setServiceId(info.getServiceId());
|
||||||
channel.setUrl(info.getUrl());
|
channel.setUrl(info.getUrl());
|
||||||
channel.setData(info.getName(),
|
channel.setName(info.getName());
|
||||||
ImageStrategy.imageListToDbUrl(info.getAvatars()),
|
channel.setAvatarUrl(ImageStrategy.imageListToDbUrl(info.getAvatars()));
|
||||||
info.getDescription(),
|
channel.setDescription(info.getDescription());
|
||||||
info.getSubscriberCount());
|
channel.setSubscriberCount(info.getSubscriberCount());
|
||||||
channelSubscription = null;
|
channelSubscription = null;
|
||||||
updateNotifyButton(null);
|
updateNotifyButton(null);
|
||||||
subscribeButtonMonitor = monitorSubscribeButton(mapOnSubscribe(channel));
|
subscribeButtonMonitor = monitorSubscribeButton(mapOnSubscribe(channel));
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
if (selectedItem instanceof PlaylistMetadataEntry) {
|
if (selectedItem instanceof PlaylistMetadataEntry) {
|
||||||
final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
|
final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
|
||||||
NavigationHelper.openLocalPlaylistFragment(fragmentManager, entry.getUid(),
|
NavigationHelper.openLocalPlaylistFragment(fragmentManager, entry.getUid(),
|
||||||
entry.name);
|
entry.getOrderingName());
|
||||||
|
|
||||||
} else if (selectedItem instanceof PlaylistRemoteEntity) {
|
} else if (selectedItem instanceof PlaylistRemoteEntity) {
|
||||||
final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem);
|
final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem);
|
||||||
@@ -148,7 +148,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
fragmentManager,
|
fragmentManager,
|
||||||
entry.getServiceId(),
|
entry.getServiceId(),
|
||||||
entry.getUrl(),
|
entry.getUrl(),
|
||||||
entry.getName());
|
entry.getOrderingName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,11 +378,11 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
|
|
||||||
if (item instanceof PlaylistMetadataEntry
|
if (item instanceof PlaylistMetadataEntry
|
||||||
&& ((PlaylistMetadataEntry) item).getDisplayIndex() != i) {
|
&& ((PlaylistMetadataEntry) item).getDisplayIndex() != i) {
|
||||||
((PlaylistMetadataEntry) item).setDisplayIndex(i);
|
((PlaylistMetadataEntry) item).setDisplayIndex((long) i);
|
||||||
localItemsUpdate.add((PlaylistMetadataEntry) item);
|
localItemsUpdate.add((PlaylistMetadataEntry) item);
|
||||||
} else if (item instanceof PlaylistRemoteEntity
|
} else if (item instanceof PlaylistRemoteEntity
|
||||||
&& ((PlaylistRemoteEntity) item).getDisplayIndex() != i) {
|
&& ((PlaylistRemoteEntity) item).getDisplayIndex() != i) {
|
||||||
((PlaylistRemoteEntity) item).setDisplayIndex(i);
|
((PlaylistRemoteEntity) item).setDisplayIndex((long) i);
|
||||||
remoteItemsUpdate.add((PlaylistRemoteEntity) item);
|
remoteItemsUpdate.add((PlaylistRemoteEntity) item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -487,7 +487,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
private void showRemoteDeleteDialog(final PlaylistRemoteEntity item) {
|
private void showRemoteDeleteDialog(final PlaylistRemoteEntity item) {
|
||||||
showDeleteDialog(item.getName(), item);
|
showDeleteDialog(item.getOrderingName(), item);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showLocalDialog(final PlaylistMetadataEntry selectedItem) {
|
private void showLocalDialog(final PlaylistMetadataEntry selectedItem) {
|
||||||
@@ -508,7 +508,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
if (items.get(index).equals(rename)) {
|
if (items.get(index).equals(rename)) {
|
||||||
showRenameDialog(selectedItem);
|
showRenameDialog(selectedItem);
|
||||||
} else if (items.get(index).equals(delete)) {
|
} else if (items.get(index).equals(delete)) {
|
||||||
showDeleteDialog(selectedItem.name, selectedItem);
|
showDeleteDialog(selectedItem.getOrderingName(), selectedItem);
|
||||||
} else if (isThumbnailPermanent && items.get(index).equals(unsetThumbnail)) {
|
} else if (isThumbnailPermanent && items.get(index).equals(unsetThumbnail)) {
|
||||||
final long thumbnailStreamId = localPlaylistManager
|
final long thumbnailStreamId = localPlaylistManager
|
||||||
.getAutomaticPlaylistThumbnailStreamId(selectedItem.getUid());
|
.getAutomaticPlaylistThumbnailStreamId(selectedItem.getUid());
|
||||||
@@ -529,7 +529,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
DialogEditTextBinding.inflate(getLayoutInflater());
|
DialogEditTextBinding.inflate(getLayoutInflater());
|
||||||
dialogBinding.dialogEditText.setHint(R.string.name);
|
dialogBinding.dialogEditText.setHint(R.string.name);
|
||||||
dialogBinding.dialogEditText.setInputType(InputType.TYPE_CLASS_TEXT);
|
dialogBinding.dialogEditText.setInputType(InputType.TYPE_CLASS_TEXT);
|
||||||
dialogBinding.dialogEditText.setText(selectedItem.name);
|
dialogBinding.dialogEditText.setText(selectedItem.getOrderingName());
|
||||||
|
|
||||||
new AlertDialog.Builder(activity)
|
new AlertDialog.Builder(activity)
|
||||||
.setView(dialogBinding.getRoot())
|
.setView(dialogBinding.getRoot())
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package org.schabi.newpipe.local.dialog;
|
package org.schabi.newpipe.local.dialog;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.DEFAULT_THUMBNAIL_ID;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@@ -14,7 +16,6 @@ import androidx.recyclerview.widget.RecyclerView;
|
|||||||
|
|
||||||
import org.schabi.newpipe.NewPipeDatabase;
|
import org.schabi.newpipe.NewPipeDatabase;
|
||||||
import org.schabi.newpipe.R;
|
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.playlist.PlaylistDuplicatesEntry;
|
||||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||||
import org.schabi.newpipe.local.LocalItemListAdapter;
|
import org.schabi.newpipe.local.LocalItemListAdapter;
|
||||||
@@ -138,7 +139,7 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
|||||||
|
|
||||||
private boolean anyPlaylistContainsDuplicates(final List<PlaylistDuplicatesEntry> playlists) {
|
private boolean anyPlaylistContainsDuplicates(final List<PlaylistDuplicatesEntry> playlists) {
|
||||||
return playlists.stream()
|
return playlists.stream()
|
||||||
.anyMatch(playlist -> playlist.timesStreamIsContained > 0);
|
.anyMatch(playlist -> playlist.getTimesStreamIsContained() > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onPlaylistSelected(@NonNull final LocalPlaylistManager manager,
|
private void onPlaylistSelected(@NonNull final LocalPlaylistManager manager,
|
||||||
@@ -146,9 +147,9 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
|||||||
@NonNull final List<StreamEntity> streams) {
|
@NonNull final List<StreamEntity> streams) {
|
||||||
|
|
||||||
final String toastText;
|
final String toastText;
|
||||||
if (playlist.timesStreamIsContained > 0) {
|
if (playlist.getTimesStreamIsContained() > 0) {
|
||||||
toastText = getString(R.string.playlist_add_stream_success_duplicate,
|
toastText = getString(R.string.playlist_add_stream_success_duplicate,
|
||||||
playlist.timesStreamIsContained);
|
playlist.getTimesStreamIsContained());
|
||||||
} else {
|
} else {
|
||||||
toastText = getString(R.string.playlist_add_stream_success);
|
toastText = getString(R.string.playlist_add_stream_success);
|
||||||
}
|
}
|
||||||
@@ -160,8 +161,9 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
|||||||
.subscribe(ignored -> {
|
.subscribe(ignored -> {
|
||||||
successToast.show();
|
successToast.show();
|
||||||
|
|
||||||
if (playlist.thumbnailUrl != null
|
if (playlist.getThumbnailStreamId() != null
|
||||||
&& playlist.thumbnailUrl.equals(PlaylistEntity.DEFAULT_THUMBNAIL)) {
|
&& playlist.getThumbnailStreamId() == DEFAULT_THUMBNAIL_ID
|
||||||
|
) {
|
||||||
playlistDisposables.add(manager
|
playlistDisposables.add(manager
|
||||||
.changePlaylistThumbnail(playlist.getUid(), streams.get(0).getUid(),
|
.changePlaylistThumbnail(playlist.getUid(), streams.get(0).getUid(),
|
||||||
false)
|
false)
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ class FeedDatabaseManager(context: Context) {
|
|||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun oldestSubscriptionUpdate(groupId: Long): Flowable<List<OffsetDateTime>> {
|
fun oldestSubscriptionUpdate(groupId: Long): Flowable<List<OffsetDateTime?>> {
|
||||||
return when (groupId) {
|
return when (groupId) {
|
||||||
FeedGroupEntity.GROUP_ALL_ID -> feedTable.oldestSubscriptionUpdateFromAll()
|
FeedGroupEntity.GROUP_ALL_ID -> feedTable.oldestSubscriptionUpdateFromAll()
|
||||||
else -> feedTable.oldestSubscriptionUpdate(groupId)
|
else -> feedTable.oldestSubscriptionUpdate(groupId)
|
||||||
|
|||||||
@@ -507,7 +507,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
|||||||
.setTitle(R.string.feed_load_error)
|
.setTitle(R.string.feed_load_error)
|
||||||
.setPositiveButton(R.string.unsubscribe) { _, _ ->
|
.setPositiveButton(R.string.unsubscribe) { _, _ ->
|
||||||
SubscriptionManager(requireContext())
|
SubscriptionManager(requireContext())
|
||||||
.deleteSubscription(subscriptionEntity.serviceId, subscriptionEntity.url)
|
.deleteSubscription(subscriptionEntity.serviceId, subscriptionEntity.url!!)
|
||||||
.subscribe()
|
.subscribe()
|
||||||
handleItemsErrors(nextItemsErrors)
|
handleItemsErrors(nextItemsErrors)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ class FeedViewModel(
|
|||||||
feedDatabaseManager.oldestSubscriptionUpdate(groupId),
|
feedDatabaseManager.oldestSubscriptionUpdate(groupId),
|
||||||
|
|
||||||
Function6 { t1: FeedEventManager.Event, t2: Boolean, t3: Boolean, t4: Boolean,
|
Function6 { t1: FeedEventManager.Event, t2: Boolean, t3: Boolean, t4: Boolean,
|
||||||
t5: Long, t6: List<OffsetDateTime> ->
|
t5: Long, t6: List<OffsetDateTime?> ->
|
||||||
return@Function6 CombineResultEventHolder(t1, t2, t3, t4, t5, t6.firstOrNull())
|
return@Function6 CombineResultEventHolder(t1, t2, t3, t4, t5, t6.firstOrNull())
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -35,15 +35,15 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder {
|
|||||||
}
|
}
|
||||||
final PlaylistMetadataEntry item = (PlaylistMetadataEntry) localItem;
|
final PlaylistMetadataEntry item = (PlaylistMetadataEntry) localItem;
|
||||||
|
|
||||||
itemTitleView.setText(item.name);
|
itemTitleView.setText(item.getOrderingName());
|
||||||
itemStreamCountView.setText(Localization.localizeStreamCountMini(
|
itemStreamCountView.setText(Localization.localizeStreamCountMini(
|
||||||
itemStreamCountView.getContext(), item.streamCount));
|
itemStreamCountView.getContext(), item.getStreamCount()));
|
||||||
itemUploaderView.setVisibility(View.INVISIBLE);
|
itemUploaderView.setVisibility(View.INVISIBLE);
|
||||||
|
|
||||||
PicassoHelper.loadPlaylistThumbnail(item.thumbnailUrl).into(itemThumbnailView);
|
PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
|
||||||
|
|
||||||
if (item instanceof PlaylistDuplicatesEntry
|
if (item instanceof PlaylistDuplicatesEntry
|
||||||
&& ((PlaylistDuplicatesEntry) item).timesStreamIsContained > 0) {
|
&& ((PlaylistDuplicatesEntry) item).getTimesStreamIsContained() > 0) {
|
||||||
itemView.setAlpha(GRAYED_OUT_ALPHA);
|
itemView.setAlpha(GRAYED_OUT_ALPHA);
|
||||||
} else {
|
} else {
|
||||||
itemView.setAlpha(1.0f);
|
itemView.setAlpha(1.0f);
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
|
|||||||
}
|
}
|
||||||
final PlaylistRemoteEntity item = (PlaylistRemoteEntity) localItem;
|
final PlaylistRemoteEntity item = (PlaylistRemoteEntity) localItem;
|
||||||
|
|
||||||
itemTitleView.setText(item.getName());
|
itemTitleView.setText(item.getOrderingName());
|
||||||
itemStreamCountView.setText(Localization.localizeStreamCountMini(
|
itemStreamCountView.setText(Localization.localizeStreamCountMini(
|
||||||
itemStreamCountView.getContext(), item.getStreamCount()));
|
itemStreamCountView.getContext(), item.getStreamCount()));
|
||||||
// Here is where the uploader name is set in the bookmarked playlists library
|
// Here is where the uploader name is set in the bookmarked playlists library
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ public class LocalPlaylistManager {
|
|||||||
|
|
||||||
public boolean getIsPlaylistThumbnailPermanent(final long playlistId) {
|
public boolean getIsPlaylistThumbnailPermanent(final long playlistId) {
|
||||||
return playlistTable.getPlaylist(playlistId).blockingFirst().get(0)
|
return playlistTable.getPlaylist(playlistId).blockingFirst().get(0)
|
||||||
.getIsThumbnailPermanent();
|
.isThumbnailPermanent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getAutomaticPlaylistThumbnailStreamId(final long playlistId) {
|
public long getAutomaticPlaylistThumbnailStreamId(final long playlistId) {
|
||||||
@@ -174,7 +174,7 @@ public class LocalPlaylistManager {
|
|||||||
}
|
}
|
||||||
if (thumbnailStreamId != THUMBNAIL_ID_LEAVE_UNCHANGED) {
|
if (thumbnailStreamId != THUMBNAIL_ID_LEAVE_UNCHANGED) {
|
||||||
playlist.setThumbnailStreamId(thumbnailStreamId);
|
playlist.setThumbnailStreamId(thumbnailStreamId);
|
||||||
playlist.setIsThumbnailPermanent(isPermanent);
|
playlist.setThumbnailPermanent(isPermanent);
|
||||||
}
|
}
|
||||||
return playlistTable.update(playlist);
|
return playlistTable.update(playlist);
|
||||||
}).subscribeOn(Schedulers.io());
|
}).subscribeOn(Schedulers.io());
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class SubscriptionManager(context: Context) {
|
|||||||
private val feedDatabaseManager = FeedDatabaseManager(context)
|
private val feedDatabaseManager = FeedDatabaseManager(context)
|
||||||
|
|
||||||
fun subscriptionTable(): SubscriptionDAO = subscriptionTable
|
fun subscriptionTable(): SubscriptionDAO = subscriptionTable
|
||||||
fun subscriptions() = subscriptionTable.all
|
fun subscriptions() = subscriptionTable.getAll()
|
||||||
|
|
||||||
fun getSubscriptions(
|
fun getSubscriptions(
|
||||||
currentGroupId: Long = FeedGroupEntity.GROUP_ALL_ID,
|
currentGroupId: Long = FeedGroupEntity.GROUP_ALL_ID,
|
||||||
@@ -44,7 +44,7 @@ class SubscriptionManager(context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
showOnlyUngrouped -> subscriptionTable.getSubscriptionsOnlyUngrouped(currentGroupId)
|
showOnlyUngrouped -> subscriptionTable.getSubscriptionsOnlyUngrouped(currentGroupId)
|
||||||
else -> subscriptionTable.all
|
else -> subscriptionTable.getAll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,12 +71,12 @@ class SubscriptionManager(context: Context) {
|
|||||||
subscriptionTable.getSubscription(info.serviceId, info.url)
|
subscriptionTable.getSubscription(info.serviceId, info.url)
|
||||||
.flatMapCompletable {
|
.flatMapCompletable {
|
||||||
Completable.fromRunnable {
|
Completable.fromRunnable {
|
||||||
it.setData(
|
it.apply {
|
||||||
info.name,
|
name = info.name
|
||||||
ImageStrategy.imageListToDbUrl(info.avatars),
|
avatarUrl = ImageStrategy.imageListToDbUrl(info.avatars)
|
||||||
info.description,
|
description = info.description
|
||||||
info.subscriberCount
|
subscriberCount = info.subscriberCount
|
||||||
)
|
}
|
||||||
subscriptionTable.update(it)
|
subscriptionTable.update(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -315,7 +315,7 @@ class MediaBrowserImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun populateHistory(): Single<List<MediaBrowserCompat.MediaItem>> {
|
private fun populateHistory(): Single<List<MediaBrowserCompat.MediaItem>> {
|
||||||
val history = database.streamHistoryDAO().getHistory().firstOrError()
|
val history = database.streamHistoryDAO().history.firstOrError()
|
||||||
return history.map { items ->
|
return history.map { items ->
|
||||||
items.map { this.createHistoryMediaItem(it) }
|
items.map { this.createHistoryMediaItem(it) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ class MediaBrowserPlaybackPreparer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val streamId = path[0].toLong()
|
val streamId = path[0].toLong()
|
||||||
return database.streamHistoryDAO().getHistory()
|
return database.streamHistoryDAO().history
|
||||||
.firstOrError()
|
.firstOrError()
|
||||||
.map { items ->
|
.map { items ->
|
||||||
val infoItems = items
|
val infoItems = items
|
||||||
|
|||||||
@@ -118,12 +118,12 @@ public class SelectPlaylistFragment extends DialogFragment {
|
|||||||
|
|
||||||
if (selectedItem instanceof PlaylistMetadataEntry) {
|
if (selectedItem instanceof PlaylistMetadataEntry) {
|
||||||
final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
|
final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
|
||||||
onSelectedListener.onLocalPlaylistSelected(entry.getUid(), entry.name);
|
onSelectedListener.onLocalPlaylistSelected(entry.getUid(), entry.getOrderingName());
|
||||||
|
|
||||||
} else if (selectedItem instanceof PlaylistRemoteEntity) {
|
} else if (selectedItem instanceof PlaylistRemoteEntity) {
|
||||||
final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem);
|
final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem);
|
||||||
onSelectedListener.onRemotePlaylistSelected(
|
onSelectedListener.onRemotePlaylistSelected(
|
||||||
entry.getServiceId(), entry.getUrl(), entry.getName());
|
entry.getServiceId(), entry.getUrl(), entry.getOrderingName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dismiss();
|
dismiss();
|
||||||
@@ -157,14 +157,15 @@ public class SelectPlaylistFragment extends DialogFragment {
|
|||||||
if (selectedItem instanceof PlaylistMetadataEntry) {
|
if (selectedItem instanceof PlaylistMetadataEntry) {
|
||||||
final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
|
final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
|
||||||
|
|
||||||
holder.titleView.setText(entry.name);
|
holder.titleView.setText(entry.getOrderingName());
|
||||||
holder.view.setOnClickListener(view -> clickedItem(position));
|
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) {
|
} else if (selectedItem instanceof PlaylistRemoteEntity) {
|
||||||
final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem);
|
final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem);
|
||||||
|
|
||||||
holder.titleView.setText(entry.getName());
|
holder.titleView.setText(entry.getOrderingName());
|
||||||
holder.view.setOnClickListener(view -> clickedItem(position));
|
holder.view.setOnClickListener(view -> clickedItem(position));
|
||||||
PicassoHelper.loadPlaylistThumbnail(entry.getThumbnailUrl())
|
PicassoHelper.loadPlaylistThumbnail(entry.getThumbnailUrl())
|
||||||
.into(holder.thumbnailView);
|
.into(holder.thumbnailView);
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class NotificationModeConfigAdapter(
|
|||||||
|
|
||||||
fun update(newData: List<SubscriptionEntity>) {
|
fun update(newData: List<SubscriptionEntity>) {
|
||||||
val items = newData.map {
|
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)
|
submitList(items)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<PlaylistMetadataEntry> localPlaylists = new ArrayList<>();
|
|
||||||
final List<PlaylistRemoteEntity> remotePlaylists = new ArrayList<>();
|
|
||||||
final List<PlaylistLocalItem> mergedPlaylists =
|
|
||||||
MergedPlaylistManager.merge(localPlaylists, remotePlaylists);
|
|
||||||
|
|
||||||
assertEquals(0, mergedPlaylists.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void onlyLocalPlaylists() {
|
|
||||||
final List<PlaylistMetadataEntry> localPlaylists = new ArrayList<>();
|
|
||||||
final List<PlaylistRemoteEntity> 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<PlaylistLocalItem> 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<PlaylistMetadataEntry> localPlaylists = new ArrayList<>();
|
|
||||||
final List<PlaylistRemoteEntity> 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<PlaylistLocalItem> 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<PlaylistMetadataEntry> localPlaylists = new ArrayList<>();
|
|
||||||
final List<PlaylistRemoteEntity> 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<PlaylistLocalItem> 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<PlaylistMetadataEntry> localPlaylists = new ArrayList<>();
|
|
||||||
final List<PlaylistRemoteEntity> 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<PlaylistLocalItem> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2022-2024 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* 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<PlaylistMetadataEntry?>()
|
||||||
|
val remotePlaylists = listOf<PlaylistRemoteEntity?>()
|
||||||
|
val mergedPlaylists = MergedPlaylistManager.merge(localPlaylists, remotePlaylists)
|
||||||
|
assertEquals(0, mergedPlaylists.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onlyLocalPlaylists() {
|
||||||
|
val localPlaylists = listOf<PlaylistMetadataEntry?>(
|
||||||
|
PlaylistMetadataEntry(1, "name1", "", 0, false, -1, 1),
|
||||||
|
PlaylistMetadataEntry(2, "name2", "", 1, false, -1, 1),
|
||||||
|
PlaylistMetadataEntry(3, "name3", "", 3, false, -1, 1)
|
||||||
|
)
|
||||||
|
val remotePlaylists = listOf<PlaylistRemoteEntity?>()
|
||||||
|
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<PlaylistMetadataEntry?>()
|
||||||
|
val remotePlaylists = listOf<PlaylistRemoteEntity?>(
|
||||||
|
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?>(
|
||||||
|
PlaylistMetadataEntry(1, "name1", "", 0, false, -1, 1),
|
||||||
|
PlaylistMetadataEntry(2, "name2", "", 1, false, -1, 1)
|
||||||
|
)
|
||||||
|
val remotePlaylists = listOf<PlaylistRemoteEntity?>(
|
||||||
|
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?>(
|
||||||
|
PlaylistMetadataEntry(1, "name1", "", 1, false, -1, 1),
|
||||||
|
PlaylistMetadataEntry(2, "name2", "", 3, false, -1, 1)
|
||||||
|
)
|
||||||
|
val remotePlaylists = listOf<PlaylistRemoteEntity?>(
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application) apply false
|
alias(libs.plugins.android.application) apply false
|
||||||
alias(libs.plugins.jetbrains.kotlin.android) 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.jetbrains.kotlin.parcelize) apply false
|
||||||
alias(libs.plugins.sonarqube) apply false
|
alias(libs.plugins.sonarqube) apply false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ jsoup = "1.21.2"
|
|||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
junit-ext = "1.1.5"
|
junit-ext = "1.1.5"
|
||||||
kotlin = "1.9.25"
|
kotlin = "1.9.25"
|
||||||
|
ksp = "1.9.25-1.0.20"
|
||||||
ktlint = "0.45.2"
|
ktlint = "0.45.2"
|
||||||
leakcanary = "2.12"
|
leakcanary = "2.12"
|
||||||
lifecycle = "2.6.2"
|
lifecycle = "2.6.2"
|
||||||
@@ -130,7 +131,7 @@ squareup-picasso = { module = "com.squareup.picasso:picasso", version.ref = "pic
|
|||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
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-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" }
|
jetbrains-kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
|
||||||
sonarqube = { id = "org.sonarqube", version.ref = "sonarqube" }
|
sonarqube = { id = "org.sonarqube", version.ref = "sonarqube" }
|
||||||
|
|||||||
Reference in New Issue
Block a user