mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-11-03 16:52:31 +01:00 
			
		
		
		
	Compare commits
	
		
			92 Commits
		
	
	
		
			v0.28.0
			...
			kspMigrati
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					7c76791db3 | ||
| 
						 | 
					995a92b7a4 | ||
| 
						 | 
					05b9ff49a2 | ||
| 
						 | 
					4422b55ab4 | ||
| 
						 | 
					f3ca5f659d | ||
| 
						 | 
					300f5abc70 | ||
| 
						 | 
					729702b420 | ||
| 
						 | 
					42f909936b | ||
| 
						 | 
					c8e294b1a3 | ||
| 
						 | 
					ee01ba3209 | ||
| 
						 | 
					1bef2fdc25 | ||
| 
						 | 
					061ce870ac | ||
| 
						 | 
					15089245bb | ||
| 
						 | 
					d99435c4ad | ||
| 
						 | 
					320c693636 | ||
| 
						 | 
					09e4bea205 | ||
| 
						 | 
					fbc664d0da | ||
| 
						 | 
					2dd4509b75 | ||
| 
						 | 
					eee1172e8a | ||
| 
						 | 
					0ebd01e65e | ||
| 
						 | 
					965eea2124 | ||
| 
						 | 
					59dfdda95e | ||
| 
						 | 
					3a2d427a46 | ||
| 
						 | 
					c25f83da6c | ||
| 
						 | 
					e2026dc378 | ||
| 
						 | 
					00f6203904 | ||
| 
						 | 
					980e8f3951 | ||
| 
						 | 
					4e9a480fdd | ||
| 
						 | 
					aa2b4821e2 | ||
| 
						 | 
					92a07a3445 | ||
| 
						 | 
					eed09f8a1d | ||
| 
						 | 
					fd3f030d0b | ||
| 
						 | 
					45c22c0db8 | ||
| 
						 | 
					2b7c72eb69 | ||
| 
						 | 
					89c4eb5237 | ||
| 
						 | 
					803aba4935 | ||
| 
						 | 
					1723bf0e8a | ||
| 
						 | 
					21e24c9e34 | ||
| 
						 | 
					eb277fe14b | ||
| 
						 | 
					d77771a60c | ||
| 
						 | 
					01f9a3de33 | ||
| 
						 | 
					150649aea9 | ||
| 
						 | 
					3803d49489 | ||
| 
						 | 
					25a4a9a253 | ||
| 
						 | 
					d534946550 | ||
| 
						 | 
					8fb3e90fe1 | ||
| 
						 | 
					5750ef6aa8 | ||
| 
						 | 
					ab7d1377e5 | ||
| 
						 | 
					fd24c08529 | ||
| 
						 | 
					e14ec3a4f9 | ||
| 
						 | 
					b592403a66 | ||
| 
						 | 
					90e1ac56ef | ||
| 
						 | 
					32eb3afe16 | ||
| 
						 | 
					83a0abddcc | ||
| 
						 | 
					35c7f2f5d1 | ||
| 
						 | 
					8afb00d2f0 | ||
| 
						 | 
					f27ec53c08 | ||
| 
						 | 
					a3ddd616f9 | ||
| 
						 | 
					79980e2078 | ||
| 
						 | 
					b204fad9d5 | ||
| 
						 | 
					08f51abefb | ||
| 
						 | 
					204df4c45a | ||
| 
						 | 
					989c0cfd28 | ||
| 
						 | 
					a369deeef4 | ||
| 
						 | 
					1bde2dcd9f | ||
| 
						 | 
					29a3ca83b5 | ||
| 
						 | 
					38064be702 | ||
| 
						 | 
					d17eae9bad | ||
| 
						 | 
					74562db965 | ||
| 
						 | 
					386d5197d8 | ||
| 
						 | 
					ccd76dea1f | ||
| 
						 | 
					9282cce6a8 | ||
| 
						 | 
					7644066c5a | ||
| 
						 | 
					9bc8139b8c | ||
| 
						 | 
					ff3526b28d | ||
| 
						 | 
					d6c0dc32d1 | ||
| 
						 | 
					124ab56c5f | ||
| 
						 | 
					56f79fac13 | ||
| 
						 | 
					ef29c318b0 | ||
| 
						 | 
					9f11db8e06 | ||
| 
						 | 
					fece0741e5 | ||
| 
						 | 
					b9b47fc520 | ||
| 
						 | 
					59db955493 | ||
| 
						 | 
					22a709d53b | ||
| 
						 | 
					329d76c857 | ||
| 
						 | 
					9f526e8e8f | ||
| 
						 | 
					50caba6606 | ||
| 
						 | 
					26443f9f14 | ||
| 
						 | 
					366129eee2 | ||
| 
						 | 
					4c8d44b6ba | ||
| 
						 | 
					14cd562ebd | ||
| 
						 | 
					04ef608f7a | 
							
								
								
									
										4
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							@@ -72,8 +72,8 @@ jobs:
 | 
			
		||||
          - api-level: 21
 | 
			
		||||
            target: default
 | 
			
		||||
            arch: x86
 | 
			
		||||
          - api-level: 33
 | 
			
		||||
            target: google_apis # emulator API 33 only exists with Google APIs
 | 
			
		||||
          - api-level: 35
 | 
			
		||||
            target: default
 | 
			
		||||
            arch: x86_64
 | 
			
		||||
 | 
			
		||||
    permissions:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										360
									
								
								app/build.gradle
									
									
									
									
									
								
							
							
						
						
									
										360
									
								
								app/build.gradle
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										296
									
								
								app/build.gradle.kts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										296
									
								
								app/build.gradle.kts
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										59
									
								
								app/check-dependencies.gradle.kts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								app/check-dependencies.gradle.kts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,59 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: 2024 NewPipe contributors <https://newpipe.net>
 | 
			
		||||
 * SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
 | 
			
		||||
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
tasks.register("checkDependenciesOrder") {
 | 
			
		||||
    group = "verification"
 | 
			
		||||
    description = "Checks that each section in libs.versions.toml is sorted alphabetically"
 | 
			
		||||
 | 
			
		||||
    val tomlFile = file("../gradle/libs.versions.toml")
 | 
			
		||||
 | 
			
		||||
    doLast {
 | 
			
		||||
        if (!tomlFile.exists()) {
 | 
			
		||||
            throw GradleException("TOML file not found")
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val lines = tomlFile.readLines()
 | 
			
		||||
        val nonSortedBlocks = mutableListOf<List<String>>()
 | 
			
		||||
        var currentBlock = mutableListOf<String>()
 | 
			
		||||
        var prevLine = ""
 | 
			
		||||
        var prevIndex = 0
 | 
			
		||||
 | 
			
		||||
        lines.forEachIndexed { lineIndex, line ->
 | 
			
		||||
            if (line.trim().isNotEmpty() && !line.startsWith("#")) {
 | 
			
		||||
                if (line.startsWith("[")) {
 | 
			
		||||
                    prevLine = ""
 | 
			
		||||
                } else {
 | 
			
		||||
                    val currIndex = lineIndex + 1
 | 
			
		||||
                    if (prevLine > line) {
 | 
			
		||||
                        if (currentBlock.isNotEmpty() && currentBlock.last() == "$prevIndex: $prevLine") {
 | 
			
		||||
                            currentBlock.add("$currIndex: $line")
 | 
			
		||||
                        } else {
 | 
			
		||||
                            if (currentBlock.isNotEmpty()) {
 | 
			
		||||
                                nonSortedBlocks.add(currentBlock)
 | 
			
		||||
                                currentBlock = mutableListOf()
 | 
			
		||||
                            }
 | 
			
		||||
                            currentBlock.add("$prevIndex: $prevLine")
 | 
			
		||||
                            currentBlock.add("$currIndex: $line")
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    prevLine = line
 | 
			
		||||
                    prevIndex = lineIndex + 1
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (currentBlock.isNotEmpty()) {
 | 
			
		||||
            nonSortedBlocks.add(currentBlock)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (nonSortedBlocks.isNotEmpty()) {
 | 
			
		||||
            throw GradleException(
 | 
			
		||||
                "The following lines were not sorted:\n" +
 | 
			
		||||
                        nonSortedBlocks.joinToString("\n\n") { it.joinToString("\n") }
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -458,7 +458,7 @@
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "name",
 | 
			
		||||
            "fieldPath": "orderingName",
 | 
			
		||||
            "columnName": "name",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": false
 | 
			
		||||
 
 | 
			
		||||
@@ -129,7 +129,7 @@ class DatabaseMigrationTest {
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        val migratedDatabaseV3 = getMigratedDatabase()
 | 
			
		||||
        val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst()
 | 
			
		||||
        val listFromDB = migratedDatabaseV3.streamDAO().getAll().blockingFirst()
 | 
			
		||||
 | 
			
		||||
        // Only expect 2, the one with the null url will be ignored
 | 
			
		||||
        assertEquals(2, listFromDB.size)
 | 
			
		||||
@@ -217,7 +217,7 @@ class DatabaseMigrationTest {
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        val migratedDatabaseV8 = getMigratedDatabase()
 | 
			
		||||
        val listFromDB = migratedDatabaseV8.searchHistoryDAO().all.blockingFirst()
 | 
			
		||||
        val listFromDB = migratedDatabaseV8.searchHistoryDAO().getAll().blockingFirst()
 | 
			
		||||
 | 
			
		||||
        assertEquals(2, listFromDB.size)
 | 
			
		||||
        assertEquals("abc", listFromDB[0].search)
 | 
			
		||||
@@ -283,8 +283,8 @@ class DatabaseMigrationTest {
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        val migratedDatabaseV9 = getMigratedDatabase()
 | 
			
		||||
        var localListFromDB = migratedDatabaseV9.playlistDAO().all.blockingFirst()
 | 
			
		||||
        var remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().all.blockingFirst()
 | 
			
		||||
        var localListFromDB = migratedDatabaseV9.playlistDAO().getAll().blockingFirst()
 | 
			
		||||
        var remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().getAll().blockingFirst()
 | 
			
		||||
 | 
			
		||||
        assertEquals(1, localListFromDB.size)
 | 
			
		||||
        assertEquals(localUid2, localListFromDB[0].uid)
 | 
			
		||||
@@ -294,17 +294,27 @@ class DatabaseMigrationTest {
 | 
			
		||||
        assertEquals(-1, remoteListFromDB[0].displayIndex)
 | 
			
		||||
 | 
			
		||||
        val localUid3 = migratedDatabaseV9.playlistDAO().insert(
 | 
			
		||||
            PlaylistEntity(DEFAULT_NAME + "3", false, -1, -1)
 | 
			
		||||
            PlaylistEntity(
 | 
			
		||||
                name = "${DEFAULT_NAME}3",
 | 
			
		||||
                isThumbnailPermanent = false,
 | 
			
		||||
                thumbnailStreamId = -1,
 | 
			
		||||
                displayIndex = -1
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
        val remoteUid3 = migratedDatabaseV9.playlistRemoteDAO().insert(
 | 
			
		||||
            PlaylistRemoteEntity(
 | 
			
		||||
                DEFAULT_THIRD_SERVICE_ID, DEFAULT_NAME, DEFAULT_THIRD_URL,
 | 
			
		||||
                DEFAULT_THUMBNAIL, DEFAULT_UPLOADER_NAME, -1, 10
 | 
			
		||||
                serviceId = DEFAULT_THIRD_SERVICE_ID,
 | 
			
		||||
                orderingName = DEFAULT_NAME,
 | 
			
		||||
                url = DEFAULT_THIRD_URL,
 | 
			
		||||
                thumbnailUrl = DEFAULT_THUMBNAIL,
 | 
			
		||||
                uploader = DEFAULT_UPLOADER_NAME,
 | 
			
		||||
                displayIndex = -1,
 | 
			
		||||
                streamCount = 10
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        localListFromDB = migratedDatabaseV9.playlistDAO().all.blockingFirst()
 | 
			
		||||
        remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().all.blockingFirst()
 | 
			
		||||
        localListFromDB = migratedDatabaseV9.playlistDAO().getAll().blockingFirst()
 | 
			
		||||
        remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().getAll().blockingFirst()
 | 
			
		||||
        assertEquals(2, localListFromDB.size)
 | 
			
		||||
        assertEquals(localUid3, localListFromDB[1].uid)
 | 
			
		||||
        assertEquals(-1, localListFromDB[1].displayIndex)
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ import org.schabi.newpipe.extractor.ServiceList;
 | 
			
		||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
 | 
			
		||||
 | 
			
		||||
import java.util.Arrays;
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
 | 
			
		||||
import static org.junit.Assert.assertEquals;
 | 
			
		||||
import static org.junit.Assert.assertTrue;
 | 
			
		||||
@@ -23,8 +24,23 @@ import static org.junit.Assert.assertTrue;
 | 
			
		||||
@LargeTest
 | 
			
		||||
public class ErrorInfoTest {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param errorInfo the error info to access
 | 
			
		||||
     * @return the private field errorInfo.message.stringRes using reflection
 | 
			
		||||
     */
 | 
			
		||||
    private int getMessageFromErrorInfo(final ErrorInfo errorInfo)
 | 
			
		||||
            throws NoSuchFieldException, IllegalAccessException {
 | 
			
		||||
        final var message = ErrorInfo.class.getDeclaredField("message");
 | 
			
		||||
        message.setAccessible(true);
 | 
			
		||||
        final var messageValue = (ErrorInfo.Companion.ErrorMessage) message.get(errorInfo);
 | 
			
		||||
 | 
			
		||||
        final var stringRes = ErrorInfo.Companion.ErrorMessage.class.getDeclaredField("stringRes");
 | 
			
		||||
        stringRes.setAccessible(true);
 | 
			
		||||
        return (int) Objects.requireNonNull(stringRes.get(messageValue));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void errorInfoTestParcelable() {
 | 
			
		||||
    public void errorInfoTestParcelable() throws NoSuchFieldException, IllegalAccessException {
 | 
			
		||||
        final ErrorInfo info = new ErrorInfo(new ParsingException("Hello"),
 | 
			
		||||
                UserAction.USER_REPORT, "request", ServiceList.YouTube.getServiceId());
 | 
			
		||||
        // Obtain a Parcel object and write the parcelable object to it:
 | 
			
		||||
@@ -39,7 +55,7 @@ public class ErrorInfoTest {
 | 
			
		||||
        assertEquals(ServiceList.YouTube.getServiceInfo().getName(),
 | 
			
		||||
                infoFromParcel.getServiceName());
 | 
			
		||||
        assertEquals("request", infoFromParcel.getRequest());
 | 
			
		||||
        assertEquals(R.string.parsing_error, infoFromParcel.getMessageStringId());
 | 
			
		||||
        assertEquals(R.string.parsing_error, getMessageFromErrorInfo(infoFromParcel));
 | 
			
		||||
 | 
			
		||||
        parcel.recycle();
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -41,7 +41,7 @@ class HistoryRecordManagerTest {
 | 
			
		||||
        // For some reason the Flowable returned by getAll() never completes, so we can't assert
 | 
			
		||||
        // that the number of Lists it returns is exactly 1, we can only check if the first List is
 | 
			
		||||
        // correct. Why on earth has a Flowable been used instead of a Single for getAll()?!?
 | 
			
		||||
        val entities = database.searchHistoryDAO().all.blockingFirst()
 | 
			
		||||
        val entities = database.searchHistoryDAO().getAll().blockingFirst()
 | 
			
		||||
        assertThat(entities).hasSize(1)
 | 
			
		||||
        assertThat(entities[0].id).isEqualTo(1)
 | 
			
		||||
        assertThat(entities[0].serviceId).isEqualTo(0)
 | 
			
		||||
@@ -51,50 +51,50 @@ class HistoryRecordManagerTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun deleteSearchHistory() {
 | 
			
		||||
        val entries = listOf(
 | 
			
		||||
            SearchHistoryEntry(time.minusSeconds(1), 0, "A"),
 | 
			
		||||
            SearchHistoryEntry(time.minusSeconds(2), 2, "A"),
 | 
			
		||||
            SearchHistoryEntry(time.minusSeconds(3), 1, "B"),
 | 
			
		||||
            SearchHistoryEntry(time.minusSeconds(4), 0, "B"),
 | 
			
		||||
            SearchHistoryEntry(creationDate = time.minusSeconds(1), serviceId = 0, search = "A"),
 | 
			
		||||
            SearchHistoryEntry(creationDate = time.minusSeconds(2), serviceId = 2, search = "A"),
 | 
			
		||||
            SearchHistoryEntry(creationDate = time.minusSeconds(3), serviceId = 1, search = "B"),
 | 
			
		||||
            SearchHistoryEntry(creationDate = time.minusSeconds(4), serviceId = 0, search = "B"),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        // make sure all 4 were inserted
 | 
			
		||||
        database.searchHistoryDAO().insertAll(entries)
 | 
			
		||||
        assertThat(database.searchHistoryDAO().all.blockingFirst()).hasSameSizeAs(entries)
 | 
			
		||||
        assertThat(database.searchHistoryDAO().getAll().blockingFirst()).hasSameSizeAs(entries)
 | 
			
		||||
 | 
			
		||||
        // try to delete only "A" entries, "B" entries should be untouched
 | 
			
		||||
        manager.deleteSearchHistory("A").test().await().assertValue(2)
 | 
			
		||||
        val entities = database.searchHistoryDAO().all.blockingFirst()
 | 
			
		||||
        val entities = database.searchHistoryDAO().getAll().blockingFirst()
 | 
			
		||||
        assertThat(entities).hasSize(2)
 | 
			
		||||
        assertThat(entities).usingElementComparator { o1, o2 -> if (o1.hasEqualValues(o2)) 0 else 1 }
 | 
			
		||||
            .containsExactly(*entries.subList(2, 4).toTypedArray())
 | 
			
		||||
 | 
			
		||||
        // assert that nothing happens if we delete a search query that does exist in the db
 | 
			
		||||
        manager.deleteSearchHistory("A").test().await().assertValue(0)
 | 
			
		||||
        val entities2 = database.searchHistoryDAO().all.blockingFirst()
 | 
			
		||||
        val entities2 = database.searchHistoryDAO().getAll().blockingFirst()
 | 
			
		||||
        assertThat(entities2).hasSize(2)
 | 
			
		||||
        assertThat(entities2).usingElementComparator { o1, o2 -> if (o1.hasEqualValues(o2)) 0 else 1 }
 | 
			
		||||
            .containsExactly(*entries.subList(2, 4).toTypedArray())
 | 
			
		||||
 | 
			
		||||
        // delete all remaining entries
 | 
			
		||||
        manager.deleteSearchHistory("B").test().await().assertValue(2)
 | 
			
		||||
        assertThat(database.searchHistoryDAO().all.blockingFirst()).isEmpty()
 | 
			
		||||
        assertThat(database.searchHistoryDAO().getAll().blockingFirst()).isEmpty()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun deleteCompleteSearchHistory() {
 | 
			
		||||
        val entries = listOf(
 | 
			
		||||
            SearchHistoryEntry(time.minusSeconds(1), 1, "A"),
 | 
			
		||||
            SearchHistoryEntry(time.minusSeconds(2), 2, "B"),
 | 
			
		||||
            SearchHistoryEntry(time.minusSeconds(3), 0, "C"),
 | 
			
		||||
            SearchHistoryEntry(creationDate = time.minusSeconds(1), serviceId = 1, search = "A"),
 | 
			
		||||
            SearchHistoryEntry(creationDate = time.minusSeconds(2), serviceId = 2, search = "B"),
 | 
			
		||||
            SearchHistoryEntry(creationDate = time.minusSeconds(3), serviceId = 0, search = "C"),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        // make sure all 3 were inserted
 | 
			
		||||
        database.searchHistoryDAO().insertAll(entries)
 | 
			
		||||
        assertThat(database.searchHistoryDAO().all.blockingFirst()).hasSameSizeAs(entries)
 | 
			
		||||
        assertThat(database.searchHistoryDAO().getAll().blockingFirst()).hasSameSizeAs(entries)
 | 
			
		||||
 | 
			
		||||
        // should remove everything
 | 
			
		||||
        manager.deleteCompleteSearchHistory().test().await().assertValue(entries.size)
 | 
			
		||||
        assertThat(database.searchHistoryDAO().all.blockingFirst()).isEmpty()
 | 
			
		||||
        assertThat(database.searchHistoryDAO().getAll().blockingFirst()).isEmpty()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun insertShuffledRelatedSearches(relatedSearches: Collection<SearchHistoryEntry>) {
 | 
			
		||||
@@ -107,7 +107,7 @@ class HistoryRecordManagerTest {
 | 
			
		||||
        // make sure all entries were inserted
 | 
			
		||||
        assertEquals(
 | 
			
		||||
            relatedSearches.size,
 | 
			
		||||
            database.searchHistoryDAO().all.blockingFirst().size
 | 
			
		||||
            database.searchHistoryDAO().getAll().blockingFirst().size
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -127,19 +127,18 @@ class HistoryRecordManagerTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun getRelatedSearches_emptyQuery_manyDuplicates() {
 | 
			
		||||
        insertShuffledRelatedSearches(
 | 
			
		||||
            listOf(
 | 
			
		||||
                SearchHistoryEntry(time.minusSeconds(9), 3, "A"),
 | 
			
		||||
                SearchHistoryEntry(time.minusSeconds(8), 3, "AB"),
 | 
			
		||||
                SearchHistoryEntry(time.minusSeconds(7), 3, "A"),
 | 
			
		||||
                SearchHistoryEntry(time.minusSeconds(6), 3, "A"),
 | 
			
		||||
                SearchHistoryEntry(time.minusSeconds(5), 3, "BA"),
 | 
			
		||||
                SearchHistoryEntry(time.minusSeconds(4), 3, "A"),
 | 
			
		||||
                SearchHistoryEntry(time.minusSeconds(3), 3, "A"),
 | 
			
		||||
                SearchHistoryEntry(time.minusSeconds(2), 0, "A"),
 | 
			
		||||
                SearchHistoryEntry(time.minusSeconds(1), 2, "AA"),
 | 
			
		||||
            )
 | 
			
		||||
        val relatedSearches = listOf(
 | 
			
		||||
            SearchHistoryEntry(creationDate = time.minusSeconds(9), serviceId = 3, search = "A"),
 | 
			
		||||
            SearchHistoryEntry(creationDate = time.minusSeconds(8), serviceId = 3, search = "AB"),
 | 
			
		||||
            SearchHistoryEntry(creationDate = time.minusSeconds(7), serviceId = 3, search = "A"),
 | 
			
		||||
            SearchHistoryEntry(creationDate = time.minusSeconds(6), serviceId = 3, search = "A"),
 | 
			
		||||
            SearchHistoryEntry(creationDate = time.minusSeconds(5), serviceId = 3, search = "BA"),
 | 
			
		||||
            SearchHistoryEntry(creationDate = time.minusSeconds(4), serviceId = 3, search = "A"),
 | 
			
		||||
            SearchHistoryEntry(creationDate = time.minusSeconds(3), serviceId = 3, search = "A"),
 | 
			
		||||
            SearchHistoryEntry(creationDate = time.minusSeconds(2), serviceId = 0, search = "A"),
 | 
			
		||||
            SearchHistoryEntry(creationDate = time.minusSeconds(1), serviceId = 2, search = "AA"),
 | 
			
		||||
        )
 | 
			
		||||
        insertShuffledRelatedSearches(relatedSearches)
 | 
			
		||||
 | 
			
		||||
        val searches = manager.getRelatedSearches("", 9, 3).blockingFirst()
 | 
			
		||||
        assertThat(searches).containsExactly("AA", "A", "BA")
 | 
			
		||||
@@ -166,13 +165,13 @@ class HistoryRecordManagerTest {
 | 
			
		||||
        private val time = OffsetDateTime.of(LocalDateTime.of(2000, 1, 1, 1, 1), ZoneOffset.UTC)
 | 
			
		||||
 | 
			
		||||
        private val RELATED_SEARCHES_ENTRIES = listOf(
 | 
			
		||||
            SearchHistoryEntry(time.minusSeconds(7), 2, "AC"),
 | 
			
		||||
            SearchHistoryEntry(time.minusSeconds(6), 0, "ABC"),
 | 
			
		||||
            SearchHistoryEntry(time.minusSeconds(5), 1, "BA"),
 | 
			
		||||
            SearchHistoryEntry(time.minusSeconds(4), 3, "A"),
 | 
			
		||||
            SearchHistoryEntry(time.minusSeconds(2), 0, "B"),
 | 
			
		||||
            SearchHistoryEntry(time.minusSeconds(3), 2, "AA"),
 | 
			
		||||
            SearchHistoryEntry(time.minusSeconds(1), 1, "A"),
 | 
			
		||||
            SearchHistoryEntry(creationDate = time.minusSeconds(7), serviceId = 2, search = "AC"),
 | 
			
		||||
            SearchHistoryEntry(creationDate = time.minusSeconds(6), serviceId = 0, search = "ABC"),
 | 
			
		||||
            SearchHistoryEntry(creationDate = time.minusSeconds(5), serviceId = 1, search = "BA"),
 | 
			
		||||
            SearchHistoryEntry(creationDate = time.minusSeconds(4), serviceId = 3, search = "A"),
 | 
			
		||||
            SearchHistoryEntry(creationDate = time.minusSeconds(2), serviceId = 0, search = "B"),
 | 
			
		||||
            SearchHistoryEntry(creationDate = time.minusSeconds(3), serviceId = 2, search = "AA"),
 | 
			
		||||
            SearchHistoryEntry(creationDate = time.minusSeconds(1), serviceId = 1, search = "A"),
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -72,6 +72,6 @@ class LocalPlaylistManagerTest {
 | 
			
		||||
        val result = manager.createPlaylist("name", listOf(stream, upserted))
 | 
			
		||||
 | 
			
		||||
        result.test().await().assertComplete()
 | 
			
		||||
        database.streamDAO().all.test().awaitCount(1).assertValue(listOf(stream, upserted))
 | 
			
		||||
        database.streamDAO().getAll().test().awaitCount(1).assertValue(listOf(stream, upserted))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,8 @@
 | 
			
		||||
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
 | 
			
		||||
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
 | 
			
		||||
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
 | 
			
		||||
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
 | 
			
		||||
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
 | 
			
		||||
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
 | 
			
		||||
 | 
			
		||||
    <!-- We need to be able to open links in the browser on API 30+ -->
 | 
			
		||||
@@ -94,9 +96,22 @@
 | 
			
		||||
            android:exported="false"
 | 
			
		||||
            android:label="@string/title_activity_about" />
 | 
			
		||||
 | 
			
		||||
        <service android:name=".local.subscription.services.SubscriptionsImportService" />
 | 
			
		||||
        <service android:name=".local.subscription.services.SubscriptionsExportService" />
 | 
			
		||||
        <service android:name=".local.feed.service.FeedLoadService" />
 | 
			
		||||
        <service
 | 
			
		||||
            android:name=".local.subscription.services.SubscriptionsImportService"
 | 
			
		||||
            android:foregroundServiceType="dataSync" />
 | 
			
		||||
 | 
			
		||||
        <service
 | 
			
		||||
            android:name=".local.subscription.services.SubscriptionsExportService"
 | 
			
		||||
            android:foregroundServiceType="dataSync" />
 | 
			
		||||
 | 
			
		||||
        <service
 | 
			
		||||
            android:name=".local.feed.service.FeedLoadService"
 | 
			
		||||
            android:foregroundServiceType="dataSync" />
 | 
			
		||||
 | 
			
		||||
        <service
 | 
			
		||||
            android:name="androidx.work.impl.foreground.SystemForegroundService"
 | 
			
		||||
            android:foregroundServiceType="dataSync"
 | 
			
		||||
            tools:node="merge" />
 | 
			
		||||
 | 
			
		||||
        <activity
 | 
			
		||||
            android:name=".PanicResponderActivity"
 | 
			
		||||
@@ -128,7 +143,8 @@
 | 
			
		||||
            android:label="@string/app_name"
 | 
			
		||||
            android:launchMode="singleTask" />
 | 
			
		||||
 | 
			
		||||
        <service android:name="us.shandian.giga.service.DownloadManagerService" />
 | 
			
		||||
        <service android:name="us.shandian.giga.service.DownloadManagerService"
 | 
			
		||||
            android:foregroundServiceType="dataSync" />
 | 
			
		||||
 | 
			
		||||
        <activity
 | 
			
		||||
            android:name=".util.FilePickerActivityHelper"
 | 
			
		||||
@@ -421,6 +437,7 @@
 | 
			
		||||
        </activity>
 | 
			
		||||
        <service
 | 
			
		||||
            android:name=".RouterActivity$FetcherService"
 | 
			
		||||
            android:foregroundServiceType="dataSync"
 | 
			
		||||
            android:exported="false" />
 | 
			
		||||
 | 
			
		||||
        <!-- opting out of sending metrics to Google in Android System WebView -->
 | 
			
		||||
 
 | 
			
		||||
@@ -48,6 +48,7 @@ import androidx.appcompat.app.ActionBar;
 | 
			
		||||
import androidx.appcompat.app.ActionBarDrawerToggle;
 | 
			
		||||
import androidx.appcompat.app.AppCompatActivity;
 | 
			
		||||
import androidx.core.app.ActivityCompat;
 | 
			
		||||
import androidx.core.content.ContextCompat;
 | 
			
		||||
import androidx.core.view.GravityCompat;
 | 
			
		||||
import androidx.drawerlayout.widget.DrawerLayout;
 | 
			
		||||
import androidx.fragment.app.Fragment;
 | 
			
		||||
@@ -896,7 +897,8 @@ public class MainActivity extends AppCompatActivity {
 | 
			
		||||
            };
 | 
			
		||||
            final IntentFilter intentFilter = new IntentFilter();
 | 
			
		||||
            intentFilter.addAction(VideoDetailFragment.ACTION_PLAYER_STARTED);
 | 
			
		||||
            registerReceiver(broadcastReceiver, intentFilter);
 | 
			
		||||
            ContextCompat.registerReceiver(this, broadcastReceiver, intentFilter,
 | 
			
		||||
                    ContextCompat.RECEIVER_EXPORTED);
 | 
			
		||||
 | 
			
		||||
            // If the PlayerHolder is not bound yet, but the service is running, try to bind to it.
 | 
			
		||||
            // Once the connection is established, the ACTION_PLAYER_STARTED will be sent.
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -58,20 +58,10 @@ import org.schabi.newpipe.extractor.NewPipe;
 | 
			
		||||
import org.schabi.newpipe.extractor.StreamingService;
 | 
			
		||||
import org.schabi.newpipe.extractor.StreamingService.LinkType;
 | 
			
		||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
 | 
			
		||||
import org.schabi.newpipe.extractor.exceptions.AgeRestrictedContentException;
 | 
			
		||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
 | 
			
		||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
 | 
			
		||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
 | 
			
		||||
import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException;
 | 
			
		||||
import org.schabi.newpipe.extractor.exceptions.PaidContentException;
 | 
			
		||||
import org.schabi.newpipe.extractor.exceptions.PrivateContentException;
 | 
			
		||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
 | 
			
		||||
import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException;
 | 
			
		||||
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException;
 | 
			
		||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
 | 
			
		||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
 | 
			
		||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
 | 
			
		||||
import org.schabi.newpipe.ktx.ExceptionUtils;
 | 
			
		||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
 | 
			
		||||
import org.schabi.newpipe.player.PlayerType;
 | 
			
		||||
import org.schabi.newpipe.player.helper.PlayerHelper;
 | 
			
		||||
@@ -260,7 +250,8 @@ public class RouterActivity extends AppCompatActivity {
 | 
			
		||||
                        showUnsupportedUrlDialog(url);
 | 
			
		||||
                    }
 | 
			
		||||
                }, throwable -> handleError(this, new ErrorInfo(throwable,
 | 
			
		||||
                        UserAction.SHARE_TO_NEWPIPE, "Getting service from url: " + url))));
 | 
			
		||||
                        UserAction.SHARE_TO_NEWPIPE, "Getting service from url: " + url,
 | 
			
		||||
                        null, url))));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -269,40 +260,19 @@ public class RouterActivity extends AppCompatActivity {
 | 
			
		||||
     * @param errorInfo the error information
 | 
			
		||||
     */
 | 
			
		||||
    private static void handleError(final Context context, final ErrorInfo errorInfo) {
 | 
			
		||||
        if (errorInfo.getThrowable() != null) {
 | 
			
		||||
            errorInfo.getThrowable().printStackTrace();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (errorInfo.getThrowable() instanceof ReCaptchaException) {
 | 
			
		||||
        if (errorInfo.getRecaptchaUrl() != null) {
 | 
			
		||||
            Toast.makeText(context, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show();
 | 
			
		||||
            // Starting ReCaptcha Challenge Activity
 | 
			
		||||
            final Intent intent = new Intent(context, ReCaptchaActivity.class);
 | 
			
		||||
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
 | 
			
		||||
            intent.putExtra(ReCaptchaActivity.RECAPTCHA_URL_EXTRA, errorInfo.getRecaptchaUrl());
 | 
			
		||||
            context.startActivity(intent);
 | 
			
		||||
        } else if (errorInfo.getThrowable() != null
 | 
			
		||||
                && ExceptionUtils.isNetworkRelated(errorInfo.getThrowable())) {
 | 
			
		||||
            Toast.makeText(context, R.string.network_error, Toast.LENGTH_LONG).show();
 | 
			
		||||
        } else if (errorInfo.getThrowable() instanceof AgeRestrictedContentException) {
 | 
			
		||||
            Toast.makeText(context, R.string.restricted_video_no_stream,
 | 
			
		||||
                    Toast.LENGTH_LONG).show();
 | 
			
		||||
        } else if (errorInfo.getThrowable() instanceof GeographicRestrictionException) {
 | 
			
		||||
            Toast.makeText(context, R.string.georestricted_content, Toast.LENGTH_LONG).show();
 | 
			
		||||
        } else if (errorInfo.getThrowable() instanceof PaidContentException) {
 | 
			
		||||
            Toast.makeText(context, R.string.paid_content, Toast.LENGTH_LONG).show();
 | 
			
		||||
        } else if (errorInfo.getThrowable() instanceof PrivateContentException) {
 | 
			
		||||
            Toast.makeText(context, R.string.private_content, Toast.LENGTH_LONG).show();
 | 
			
		||||
        } else if (errorInfo.getThrowable() instanceof SoundCloudGoPlusContentException) {
 | 
			
		||||
            Toast.makeText(context, R.string.soundcloud_go_plus_content,
 | 
			
		||||
                    Toast.LENGTH_LONG).show();
 | 
			
		||||
        } else if (errorInfo.getThrowable() instanceof YoutubeMusicPremiumContentException) {
 | 
			
		||||
            Toast.makeText(context, R.string.youtube_music_premium_content,
 | 
			
		||||
                    Toast.LENGTH_LONG).show();
 | 
			
		||||
        } else if (errorInfo.getThrowable() instanceof ContentNotAvailableException) {
 | 
			
		||||
            Toast.makeText(context, R.string.content_not_available, Toast.LENGTH_LONG).show();
 | 
			
		||||
        } else if (errorInfo.getThrowable() instanceof ContentNotSupportedException) {
 | 
			
		||||
            Toast.makeText(context, R.string.content_not_supported, Toast.LENGTH_LONG).show();
 | 
			
		||||
        } else {
 | 
			
		||||
        } else if (errorInfo.isReportable()) {
 | 
			
		||||
            ErrorUtil.createNotification(context, errorInfo);
 | 
			
		||||
        } else {
 | 
			
		||||
            // this exception does not usually indicate a problem that should be reported,
 | 
			
		||||
            // so just show a toast instead of the notification
 | 
			
		||||
            Toast.makeText(context, errorInfo.getMessage(context), Toast.LENGTH_LONG).show();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (context instanceof RouterActivity) {
 | 
			
		||||
@@ -665,7 +635,8 @@ public class RouterActivity extends AppCompatActivity {
 | 
			
		||||
                        startActivity(intent);
 | 
			
		||||
                        finish();
 | 
			
		||||
                    }, throwable -> handleError(this, new ErrorInfo(throwable,
 | 
			
		||||
                            UserAction.SHARE_TO_NEWPIPE, "Starting info activity: " + currentUrl)))
 | 
			
		||||
                            UserAction.SHARE_TO_NEWPIPE, "Starting info activity: " + currentUrl,
 | 
			
		||||
                            null, currentUrl)))
 | 
			
		||||
            );
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
@@ -852,10 +823,10 @@ public class RouterActivity extends AppCompatActivity {
 | 
			
		||||
                                            })
 | 
			
		||||
                                    )),
 | 
			
		||||
                            throwable -> runOnVisible(ctx -> handleError(ctx, new ErrorInfo(
 | 
			
		||||
                                    throwable,
 | 
			
		||||
                                    UserAction.REQUESTED_STREAM,
 | 
			
		||||
                                    throwable, UserAction.REQUESTED_STREAM,
 | 
			
		||||
                                    "Tried to add " + currentUrl + " to a playlist",
 | 
			
		||||
                                    ((RouterActivity) ctx).currentService.getServiceId())
 | 
			
		||||
                                    ((RouterActivity) ctx).currentService.getServiceId(),
 | 
			
		||||
                                    currentUrl)
 | 
			
		||||
                            ))
 | 
			
		||||
                    )
 | 
			
		||||
            );
 | 
			
		||||
@@ -995,7 +966,7 @@ public class RouterActivity extends AppCompatActivity {
 | 
			
		||||
                            }
 | 
			
		||||
                        }, throwable -> handleError(this, new ErrorInfo(throwable, finalUserAction,
 | 
			
		||||
                                choice.url + " opened with " + choice.playerChoice,
 | 
			
		||||
                                choice.serviceId)));
 | 
			
		||||
                                choice.serviceId, choice.url)));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										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
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -168,10 +168,10 @@ abstract class FeedDAO {
 | 
			
		||||
        ON fgs.subscription_id = lu.subscription_id AND fgs.group_id = :groupId
 | 
			
		||||
        """
 | 
			
		||||
    )
 | 
			
		||||
    abstract fun oldestSubscriptionUpdate(groupId: Long): Flowable<List<OffsetDateTime>>
 | 
			
		||||
    abstract fun oldestSubscriptionUpdate(groupId: Long): Flowable<List<OffsetDateTime?>>
 | 
			
		||||
 | 
			
		||||
    @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")
 | 
			
		||||
    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
 | 
			
		||||
 | 
			
		||||
import androidx.room.ColumnInfo
 | 
			
		||||
@@ -11,23 +17,24 @@ import java.time.OffsetDateTime
 | 
			
		||||
    tableName = SearchHistoryEntry.TABLE_NAME,
 | 
			
		||||
    indices = [Index(value = [SearchHistoryEntry.SEARCH])]
 | 
			
		||||
)
 | 
			
		||||
data class SearchHistoryEntry(
 | 
			
		||||
    @field:ColumnInfo(name = CREATION_DATE) var creationDate: OffsetDateTime?,
 | 
			
		||||
    @field:ColumnInfo(
 | 
			
		||||
        name = SERVICE_ID
 | 
			
		||||
    ) var serviceId: Int,
 | 
			
		||||
    @field:ColumnInfo(name = SEARCH) var search: String?
 | 
			
		||||
) {
 | 
			
		||||
data class SearchHistoryEntry @JvmOverloads constructor(
 | 
			
		||||
    @ColumnInfo(name = CREATION_DATE)
 | 
			
		||||
    var creationDate: OffsetDateTime?,
 | 
			
		||||
 | 
			
		||||
    @ColumnInfo(name = SERVICE_ID)
 | 
			
		||||
    val serviceId: Int,
 | 
			
		||||
 | 
			
		||||
    @ColumnInfo(name = SEARCH)
 | 
			
		||||
    val search: String?,
 | 
			
		||||
 | 
			
		||||
    @ColumnInfo(name = ID)
 | 
			
		||||
    @PrimaryKey(autoGenerate = true)
 | 
			
		||||
    var id: Long = 0
 | 
			
		||||
    val id: Long = 0,
 | 
			
		||||
) {
 | 
			
		||||
 | 
			
		||||
    @Ignore
 | 
			
		||||
    fun hasEqualValues(otherEntry: SearchHistoryEntry): Boolean {
 | 
			
		||||
        return (
 | 
			
		||||
            serviceId == otherEntry.serviceId &&
 | 
			
		||||
                search == otherEntry.search
 | 
			
		||||
            )
 | 
			
		||||
        return serviceId == otherEntry.serviceId && search == otherEntry.search
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
 
 | 
			
		||||
@@ -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"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user