mirror of
https://github.com/TeamNewPipe/NewPipe.git
synced 2025-12-05 01:10:43 +00:00
Merge branch 'dev' into refactor
This commit is contained in:
45
.editorconfig
Normal file
45
.editorconfig
Normal file
@@ -0,0 +1,45 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
|
||||
root = true
|
||||
|
||||
[*.{kt,kts}]
|
||||
ktlint_function_naming_ignore_when_annotated_with = Composable
|
||||
ktlint_standard_annotation = disabled
|
||||
ktlint_standard_argument-list-wrapping = disabled
|
||||
ktlint_standard_backing-property-naming = disabled
|
||||
ktlint_standard_blank-line-before-declaration = disabled
|
||||
ktlint_standard_blank-line-between-when-conditions = disabled
|
||||
ktlint_standard_chain-method-continuation = disabled
|
||||
ktlint_standard_class-signature = disabled
|
||||
ktlint_standard_comment-wrapping = disabled
|
||||
ktlint_standard_enum-wrapping = disabled
|
||||
ktlint_standard_function-expression-body = disabled
|
||||
ktlint_standard_function-literal = disabled
|
||||
ktlint_standard_function-signature = disabled
|
||||
ktlint_standard_indent = disabled
|
||||
ktlint_standard_kdoc = disabled
|
||||
ktlint_standard_max-line-length = disabled
|
||||
ktlint_standard_mixed-condition-operators = disabled
|
||||
ktlint_standard_multiline-expression-wrapping = disabled
|
||||
ktlint_standard_multiline-if-else = disabled
|
||||
ktlint_standard_no-blank-line-in-list = disabled
|
||||
ktlint_standard_no-consecutive-comments = disabled
|
||||
ktlint_standard_no-empty-first-line-in-class-body = disabled
|
||||
ktlint_standard_no-empty-first-line-in-method-block = disabled
|
||||
ktlint_standard_no-line-break-after-else = disabled
|
||||
ktlint_standard_no-semi = disabled
|
||||
ktlint_standard_no-single-line-block-comment = disabled
|
||||
ktlint_standard_package-name = disabled
|
||||
ktlint_standard_parameter-list-wrapping = disabled
|
||||
ktlint_standard_property-naming = disabled
|
||||
ktlint_standard_spacing-between-declarations-with-annotations = disabled
|
||||
ktlint_standard_spacing-between-declarations-with-comments = disabled
|
||||
ktlint_standard_statement-wrapping = disabled
|
||||
ktlint_standard_string-template-indent = disabled
|
||||
ktlint_standard_trailing-comma-on-call-site = disabled
|
||||
ktlint_standard_trailing-comma-on-declaration-site = disabled
|
||||
ktlint_standard_try-catch-finally-spacing = disabled
|
||||
ktlint_standard_when-entry-bracing = disabled
|
||||
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -2,7 +2,7 @@
|
||||
|
||||
#### What is it?
|
||||
- [ ] Bugfix (user facing)
|
||||
- [ ] Feature (user facing)
|
||||
- [ ] Feature (user facing) ⚠️ **Your PR must target the [`refactor`](https://github.com/TeamNewPipe/NewPipe/tree/refactor) branch**
|
||||
- [ ] Codebase improvement (dev facing)
|
||||
- [ ] Meta improvement to the project (dev facing)
|
||||
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,6 +7,7 @@ captures/
|
||||
*.iml
|
||||
*~
|
||||
.weblate
|
||||
.kotlin
|
||||
*.class
|
||||
app/debug/
|
||||
app/release/
|
||||
|
||||
387
app/build.gradle
387
app/build.gradle
@@ -1,387 +0,0 @@
|
||||
import com.android.tools.profgen.ArtProfileKt
|
||||
import com.android.tools.profgen.ArtProfileSerializer
|
||||
import com.android.tools.profgen.DexFile
|
||||
import com.mikepenz.aboutlibraries.plugin.DuplicateMode
|
||||
|
||||
plugins {
|
||||
alias libs.plugins.android.application
|
||||
alias libs.plugins.kotlin.android
|
||||
alias libs.plugins.kotlin.compose
|
||||
alias libs.plugins.kotlin.kapt
|
||||
alias libs.plugins.kotlin.parcelize
|
||||
alias libs.plugins.kotlinx.serialization
|
||||
alias libs.plugins.checkstyle
|
||||
alias libs.plugins.sonarqube
|
||||
alias libs.plugins.hilt
|
||||
alias libs.plugins.aboutlibraries
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk 36
|
||||
namespace 'org.schabi.newpipe'
|
||||
|
||||
defaultConfig {
|
||||
applicationId "org.schabi.newpipe"
|
||||
resValue "string", "app_name", "NewPipe"
|
||||
minSdk 21
|
||||
targetSdk 35
|
||||
if (System.properties.containsKey('versionCodeOverride')) {
|
||||
versionCode System.getProperty('versionCodeOverride') as Integer
|
||||
} else {
|
||||
versionCode 1005
|
||||
}
|
||||
versionName "0.28.0"
|
||||
if (System.properties.containsKey('versionNameSuffix')) {
|
||||
versionNameSuffix System.getProperty('versionNameSuffix')
|
||||
}
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
javaCompileOptions {
|
||||
annotationProcessorOptions {
|
||||
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
debuggable true
|
||||
|
||||
// suffix the app id and the app name with git branch name
|
||||
def workingBranch = getGitWorkingBranch()
|
||||
def normalizedWorkingBranch = workingBranch.replaceFirst("^[^A-Za-z]+", "").replaceAll("[^0-9A-Za-z]+", "")
|
||||
if (normalizedWorkingBranch.isEmpty() || workingBranch == "master" || workingBranch == "dev") {
|
||||
// default values when branch name could not be determined or is master or dev
|
||||
applicationIdSuffix ".debug"
|
||||
resValue "string", "app_name", "NewPipe Debug"
|
||||
} else {
|
||||
applicationIdSuffix ".debug." + normalizedWorkingBranch
|
||||
resValue "string", "app_name", "NewPipe " + workingBranch
|
||||
archivesBaseName = 'NewPipe_' + normalizedWorkingBranch
|
||||
}
|
||||
}
|
||||
|
||||
release {
|
||||
if (System.properties.containsKey('packageSuffix')) {
|
||||
applicationIdSuffix System.getProperty('packageSuffix')
|
||||
resValue "string", "app_name", "NewPipe " + System.getProperty('packageSuffix')
|
||||
archivesBaseName = 'NewPipe_' + System.getProperty('packageSuffix')
|
||||
}
|
||||
minifyEnabled true
|
||||
shrinkResources false // disabled to fix F-Droid's reproducible build
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
archivesBaseName = 'app'
|
||||
}
|
||||
}
|
||||
|
||||
lint {
|
||||
checkReleaseBuilds false
|
||||
// Or, if you prefer, you can continue to check for errors in release builds,
|
||||
// but continue the build even when errors are found:
|
||||
abortOnError false
|
||||
// suppress false warning ("Resource IDs will be non-final in Android Gradle Plugin version
|
||||
// 5.0, avoid using them in switch case statements"), which affects only library projects
|
||||
disable 'NonConstantResourceId'
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
// Flag to enable support for the new language APIs
|
||||
coreLibraryDesugaringEnabled true
|
||||
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
encoding 'utf-8'
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
|
||||
}
|
||||
|
||||
androidResources {
|
||||
generateLocaleConfig = true
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
compose true
|
||||
buildConfig true
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
resources {
|
||||
// remove two files which belong to jsoup
|
||||
// no idea how they ended up in the META-INF dir...
|
||||
excludes += ['META-INF/README.md', 'META-INF/CHANGES',
|
||||
// 'COPYRIGHT' belongs to RxJava...
|
||||
'META-INF/COPYRIGHT']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
configurations {
|
||||
checkstyle
|
||||
ktlint
|
||||
}
|
||||
|
||||
checkstyle {
|
||||
getConfigDirectory().set(rootProject.file("checkstyle"))
|
||||
ignoreFailures false
|
||||
showViolations true
|
||||
toolVersion = libs.versions.checkstyle.get()
|
||||
}
|
||||
|
||||
tasks.register('runCheckstyle', Checkstyle) {
|
||||
source 'src'
|
||||
include '**/*.java'
|
||||
exclude '**/gen/**'
|
||||
exclude '**/R.java'
|
||||
exclude '**/BuildConfig.java'
|
||||
exclude 'main/java/us/shandian/giga/**'
|
||||
|
||||
classpath = configurations.checkstyle
|
||||
|
||||
showViolations true
|
||||
|
||||
reports {
|
||||
xml.getRequired().set(true)
|
||||
html.getRequired().set(true)
|
||||
}
|
||||
}
|
||||
|
||||
def outputDir = "${project.buildDir}/reports/ktlint/"
|
||||
def inputFiles = project.fileTree(dir: "src", include: "**/*.kt")
|
||||
|
||||
tasks.register('runKtlint', JavaExec) {
|
||||
inputs.files(inputFiles)
|
||||
outputs.dir(outputDir)
|
||||
getMainClass().set("com.pinterest.ktlint.Main")
|
||||
classpath = configurations.ktlint
|
||||
args "src/**/*.kt"
|
||||
jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED")
|
||||
}
|
||||
|
||||
tasks.register('formatKtlint', JavaExec) {
|
||||
inputs.files(inputFiles)
|
||||
outputs.dir(outputDir)
|
||||
getMainClass().set("com.pinterest.ktlint.Main")
|
||||
classpath = configurations.ktlint
|
||||
args "-F", "src/**/*.kt"
|
||||
jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED")
|
||||
}
|
||||
|
||||
apply from: 'check-dependencies.gradle'
|
||||
|
||||
afterEvaluate {
|
||||
if (!System.properties.containsKey('skipFormatKtlint')) {
|
||||
preDebugBuild.dependsOn formatKtlint
|
||||
}
|
||||
preDebugBuild.dependsOn runCheckstyle, runKtlint, checkDependenciesOrder
|
||||
}
|
||||
|
||||
sonar {
|
||||
properties {
|
||||
property "sonar.projectKey", "TeamNewPipe_NewPipe"
|
||||
property "sonar.organization", "teamnewpipe"
|
||||
property "sonar.host.url", "https://sonarcloud.io"
|
||||
}
|
||||
}
|
||||
|
||||
kapt {
|
||||
correctErrorTypes true
|
||||
}
|
||||
|
||||
aboutLibraries {
|
||||
// note: offline mode prevents the plugin from fetching licenses at build time, which would be
|
||||
// harmful for reproducible builds
|
||||
offlineMode = true
|
||||
duplicationMode = DuplicateMode.MERGE
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** Desugaring **/
|
||||
coreLibraryDesugaring libs.desugar.jdk.libs.nio
|
||||
|
||||
/** NewPipe libraries **/
|
||||
implementation libs.teamnewpipe.nanojson
|
||||
implementation libs.teamnewpipe.newpipe.extractor
|
||||
implementation libs.teamnewpipe.nononsense.filepicker
|
||||
|
||||
/** Checkstyle **/
|
||||
checkstyle libs.tools.checkstyle
|
||||
ktlint libs.tools.ktlint
|
||||
|
||||
/** Kotlin **/
|
||||
implementation libs.kotlin.stdlib
|
||||
|
||||
/** AndroidX **/
|
||||
implementation libs.androidx.appcompat
|
||||
implementation libs.androidx.cardview
|
||||
implementation libs.androidx.constraintlayout
|
||||
implementation libs.androidx.core.ktx
|
||||
implementation libs.androidx.documentfile
|
||||
implementation libs.androidx.fragment.compose
|
||||
implementation libs.androidx.lifecycle.livedata
|
||||
implementation libs.androidx.lifecycle.viewmodel
|
||||
implementation libs.androidx.media
|
||||
implementation libs.androidx.preference
|
||||
implementation libs.androidx.recyclerview
|
||||
implementation libs.androidx.room.runtime
|
||||
implementation libs.androidx.room.rxjava3
|
||||
kapt libs.androidx.room.compiler
|
||||
implementation libs.androidx.swiperefreshlayout
|
||||
implementation libs.androidx.work.runtime
|
||||
implementation libs.androidx.work.rxjava3
|
||||
implementation libs.androidx.material
|
||||
implementation libs.androidx.webkit
|
||||
|
||||
/** Third-party libraries **/
|
||||
// Instance state boilerplate elimination
|
||||
implementation libs.livefront.bridge
|
||||
implementation libs.android.state
|
||||
kapt libs.android.state.processor
|
||||
|
||||
// HTML parser
|
||||
implementation libs.jsoup
|
||||
|
||||
// HTTP client
|
||||
implementation libs.okhttp
|
||||
|
||||
// Media player
|
||||
implementation libs.exoplayer.core
|
||||
implementation libs.exoplayer.dash
|
||||
implementation libs.exoplayer.database
|
||||
implementation libs.exoplayer.datasource
|
||||
implementation libs.exoplayer.hls
|
||||
implementation libs.exoplayer.smoothstreaming
|
||||
implementation libs.exoplayer.ui
|
||||
implementation libs.extension.mediasession
|
||||
|
||||
// Metadata generator for service descriptors
|
||||
compileOnly libs.auto.service
|
||||
kapt libs.auto.service.kapt
|
||||
|
||||
// Manager for complex RecyclerView layouts
|
||||
implementation libs.lisawray.groupie
|
||||
implementation libs.lisawray.groupie.viewbinding
|
||||
|
||||
// Image loading
|
||||
implementation libs.coil.compose
|
||||
implementation libs.coil.network.okhttp
|
||||
|
||||
// Markdown library for Android
|
||||
implementation libs.markwon.core
|
||||
implementation libs.markwon.linkify
|
||||
|
||||
// Crash reporting
|
||||
implementation libs.acra.core
|
||||
|
||||
// Properly restarting
|
||||
implementation libs.process.phoenix
|
||||
|
||||
// Reactive extensions for Java VM
|
||||
implementation libs.rxjava3.rxjava
|
||||
implementation libs.rxjava3.rxandroid
|
||||
// RxJava binding APIs for Android UI widgets
|
||||
implementation libs.rxbinding4.rxbinding
|
||||
|
||||
// Date and time formatting
|
||||
implementation libs.prettytime
|
||||
|
||||
// Jetpack Compose
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation libs.androidx.compose.material3
|
||||
implementation libs.androidx.compose.adaptive
|
||||
implementation libs.androidx.activity.compose
|
||||
implementation libs.androidx.compose.ui.tooling.preview
|
||||
implementation libs.androidx.lifecycle.viewmodel.compose
|
||||
implementation libs.androidx.compose.ui.text // Needed for parsing HTML to AnnotatedString
|
||||
implementation libs.androidx.compose.material.icons.extended
|
||||
|
||||
// Jetpack Compose related dependencies
|
||||
implementation libs.androidx.paging.compose
|
||||
implementation libs.androidx.navigation.compose
|
||||
|
||||
// Coroutines interop
|
||||
implementation libs.kotlinx.coroutines.rx3
|
||||
|
||||
// Library loading for About screen
|
||||
implementation libs.aboutlibraries.compose.m3
|
||||
|
||||
// Hilt
|
||||
implementation libs.hilt.android
|
||||
kapt(libs.hilt.compiler)
|
||||
|
||||
// Scroll
|
||||
implementation libs.lazycolumnscrollbar
|
||||
|
||||
// Kotlinx Serialization
|
||||
implementation libs.kotlinx.serialization.json
|
||||
|
||||
/** Debugging **/
|
||||
// Memory leak detection
|
||||
debugImplementation libs.leakcanary.object.watcher
|
||||
debugImplementation libs.leakcanary.plumber.android
|
||||
debugImplementation libs.leakcanary.android.core
|
||||
// Debug bridge for Android
|
||||
debugImplementation libs.stetho
|
||||
debugImplementation libs.stetho.okhttp3
|
||||
|
||||
// Jetpack Compose
|
||||
debugImplementation libs.androidx.compose.ui.tooling
|
||||
|
||||
/** Testing **/
|
||||
testImplementation libs.junit
|
||||
testImplementation libs.mockito.core
|
||||
|
||||
androidTestImplementation libs.androidx.junit
|
||||
androidTestImplementation libs.androidx.runner
|
||||
androidTestImplementation libs.androidx.room.testing
|
||||
androidTestImplementation libs.assertj.core
|
||||
androidTestImplementation platform(libs.androidx.compose.bom)
|
||||
androidTestImplementation libs.androidx.compose.ui.test.junit4
|
||||
debugImplementation libs.androidx.compose.ui.test.manifest
|
||||
|
||||
}
|
||||
|
||||
static String getGitWorkingBranch() {
|
||||
try {
|
||||
def gitProcess = "git rev-parse --abbrev-ref HEAD".execute()
|
||||
gitProcess.waitFor()
|
||||
if (gitProcess.exitValue() == 0) {
|
||||
return gitProcess.text.trim()
|
||||
} else {
|
||||
// not a git repository
|
||||
return ""
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
// git was not found
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// fix reproducible builds
|
||||
project.afterEvaluate {
|
||||
tasks.compileReleaseArtProfile.doLast {
|
||||
outputs.files.each { file ->
|
||||
if (file.toString().endsWith(".profm")) {
|
||||
println("Sorting ${file} ...")
|
||||
def version = ArtProfileSerializer.valueOf("METADATA_0_0_2")
|
||||
def profile = ArtProfileKt.ArtProfile(file)
|
||||
def keys = new ArrayList(profile.profileData.keySet())
|
||||
def sortedData = new LinkedHashMap()
|
||||
Collections.sort keys, new DexFile.Companion()
|
||||
keys.each { key -> sortedData[key] = profile.profileData[key] }
|
||||
new FileOutputStream(file).with {
|
||||
write(version.magicBytes$profgen)
|
||||
write(version.versionBytes$profgen)
|
||||
version.write$profgen(it, sortedData, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
361
app/build.gradle.kts
Normal file
361
app/build.gradle.kts
Normal file
@@ -0,0 +1,361 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
import com.mikepenz.aboutlibraries.plugin.DuplicateMode
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.jetbrains.kotlin.android)
|
||||
alias(libs.plugins.jetbrains.kotlin.compose)
|
||||
alias(libs.plugins.jetbrains.kotlin.kapt)
|
||||
alias(libs.plugins.jetbrains.kotlin.parcelize)
|
||||
alias(libs.plugins.jetbrains.kotlinx.serialization)
|
||||
alias(libs.plugins.google.ksp)
|
||||
alias(libs.plugins.sonarqube)
|
||||
alias(libs.plugins.hilt)
|
||||
alias(libs.plugins.about.libraries)
|
||||
checkstyle
|
||||
}
|
||||
|
||||
val gitWorkingBranch = providers.exec {
|
||||
commandLine("git", "rev-parse", "--abbrev-ref", "HEAD")
|
||||
}.standardOutput.asText.map { it.trim() }
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(17)
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
// TODO: Drop annotation default target when it is stable
|
||||
freeCompilerArgs.addAll(
|
||||
"-Xannotation-default-target=param-property"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = 36
|
||||
namespace = "org.schabi.newpipe"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "org.schabi.newpipe"
|
||||
resValue("string", "app_name", "NewPipe")
|
||||
minSdk = 21
|
||||
targetSdk = 35
|
||||
|
||||
versionCode = System.getProperty("versionCodeOverride")?.toInt() ?: 1005
|
||||
|
||||
versionName = "0.28.0"
|
||||
System.getProperty("versionNameSuffix")?.let { versionNameSuffix = it }
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
isDebuggable = true
|
||||
|
||||
// suffix the app id and the app name with git branch name
|
||||
val defaultBranches = listOf("master", "dev")
|
||||
val workingBranch = gitWorkingBranch.getOrElse("")
|
||||
val normalizedWorkingBranch = workingBranch
|
||||
.replaceFirst("^[^A-Za-z]+".toRegex(), "")
|
||||
.replace("[^0-9A-Za-z]+".toRegex(), "")
|
||||
|
||||
if (normalizedWorkingBranch.isEmpty() || workingBranch in defaultBranches) {
|
||||
// default values when branch name could not be determined or is master or dev
|
||||
applicationIdSuffix = ".debug"
|
||||
resValue("string", "app_name", "NewPipe Debug")
|
||||
} else {
|
||||
applicationIdSuffix = ".debug.$normalizedWorkingBranch"
|
||||
resValue("string", "app_name", "NewPipe $workingBranch")
|
||||
}
|
||||
}
|
||||
|
||||
release {
|
||||
System.getProperty("packageSuffix")?.let { suffix ->
|
||||
applicationIdSuffix = suffix
|
||||
resValue("string", "app_name", "NewPipe $suffix")
|
||||
}
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = false // disabled to fix F-Droid"s reproducible build
|
||||
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
|
||||
}
|
||||
}
|
||||
|
||||
lint {
|
||||
checkReleaseBuilds = false
|
||||
// Or, if you prefer, you can continue to check for errors in release builds,
|
||||
// but continue the build even when errors are found:
|
||||
abortOnError = false
|
||||
// suppress false warning ("Resource IDs will be non-final in Android Gradle Plugin version
|
||||
// 5.0, avoid using them in switch case statements"), which affects only library projects
|
||||
disable += "NonConstantResourceId"
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
// Flag to enable support for the new language APIs
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
encoding = "utf-8"
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
getByName("androidTest") {
|
||||
assets.srcDir("$projectDir/schemas")
|
||||
}
|
||||
}
|
||||
|
||||
androidResources {
|
||||
generateLocaleConfig = true
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
compose = true
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources {
|
||||
// remove two files which belong to jsoup
|
||||
// no idea how they ended up in the META-INF dir...
|
||||
excludes += setOf(
|
||||
"META-INF/README.md",
|
||||
"META-INF/CHANGES",
|
||||
"META-INF/COPYRIGHT" // "COPYRIGHT" belongs to RxJava...
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ksp {
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
}
|
||||
|
||||
|
||||
// Custom dependency configuration for ktlint
|
||||
val ktlint by configurations.creating
|
||||
|
||||
checkstyle {
|
||||
configDirectory = rootProject.file("checkstyle")
|
||||
isIgnoreFailures = false
|
||||
isShowViolations = true
|
||||
toolVersion = libs.versions.checkstyle.get()
|
||||
}
|
||||
|
||||
tasks.register<Checkstyle>("runCheckstyle") {
|
||||
source("src")
|
||||
include("**/*.java")
|
||||
exclude("**/gen/**")
|
||||
exclude("**/R.java")
|
||||
exclude("**/BuildConfig.java")
|
||||
exclude("main/java/us/shandian/giga/**")
|
||||
|
||||
classpath = configurations.getByName("checkstyle")
|
||||
|
||||
isShowViolations = true
|
||||
|
||||
reports {
|
||||
xml.required = true
|
||||
html.required = true
|
||||
}
|
||||
}
|
||||
|
||||
val outputDir = project.layout.buildDirectory.dir("reports/ktlint/")
|
||||
val inputFiles = fileTree("src") { include("**/*.kt") }
|
||||
|
||||
tasks.register<JavaExec>("runKtlint") {
|
||||
inputs.files(inputFiles)
|
||||
outputs.dir(outputDir)
|
||||
mainClass.set("com.pinterest.ktlint.Main")
|
||||
classpath = configurations.getByName("ktlint")
|
||||
args = listOf("--editorconfig=../.editorconfig", "src/**/*.kt")
|
||||
jvmArgs = listOf("--add-opens", "java.base/java.lang=ALL-UNNAMED")
|
||||
}
|
||||
|
||||
tasks.register<JavaExec>("formatKtlint") {
|
||||
inputs.files(inputFiles)
|
||||
outputs.dir(outputDir)
|
||||
mainClass.set("com.pinterest.ktlint.Main")
|
||||
classpath = configurations.getByName("ktlint")
|
||||
args = listOf("--editorconfig=../.editorconfig", "-F", "src/**/*.kt")
|
||||
jvmArgs = listOf("--add-opens", "java.base/java.lang=ALL-UNNAMED")
|
||||
}
|
||||
|
||||
tasks.register<CheckDependenciesOrder>("checkDependenciesOrder") {
|
||||
tomlFile = layout.projectDirectory.file("../gradle/libs.versions.toml")
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
tasks.named("preDebugBuild").configure {
|
||||
if (!System.getProperties().containsKey("skipFormatKtlint")) {
|
||||
dependsOn("formatKtlint")
|
||||
}
|
||||
dependsOn("runCheckstyle", "runKtlint", "checkDependenciesOrder")
|
||||
}
|
||||
}
|
||||
|
||||
sonar {
|
||||
properties {
|
||||
property("sonar.projectKey", "TeamNewPipe_NewPipe")
|
||||
property("sonar.organization", "teamnewpipe")
|
||||
property("sonar.host.url", "https://sonarcloud.io")
|
||||
}
|
||||
}
|
||||
|
||||
aboutLibraries {
|
||||
// note: offline mode prevents the plugin from fetching licenses at build time, which would be
|
||||
// harmful for reproducible builds
|
||||
offlineMode = true
|
||||
duplicationMode = DuplicateMode.MERGE
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** Desugaring **/
|
||||
coreLibraryDesugaring(libs.android.desugar)
|
||||
|
||||
/** NewPipe libraries **/
|
||||
implementation(libs.newpipe.nanojson)
|
||||
implementation(libs.newpipe.extractor)
|
||||
implementation(libs.newpipe.filepicker)
|
||||
|
||||
/** Checkstyle **/
|
||||
checkstyle(libs.puppycrawl.checkstyle)
|
||||
ktlint(libs.pinterest.ktlint)
|
||||
|
||||
/** Kotlin **/
|
||||
implementation(libs.kotlin.stdlib)
|
||||
|
||||
/** AndroidX **/
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.androidx.cardview)
|
||||
implementation(libs.androidx.constraintlayout)
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.documentfile)
|
||||
implementation(libs.androidx.fragment.compose)
|
||||
implementation(libs.androidx.lifecycle.livedata)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.ktx)
|
||||
implementation(libs.androidx.media)
|
||||
implementation(libs.androidx.preference)
|
||||
implementation(libs.androidx.recyclerview)
|
||||
implementation(libs.androidx.room.runtime)
|
||||
implementation(libs.androidx.room.rxjava3)
|
||||
ksp(libs.androidx.room.compiler)
|
||||
implementation(libs.androidx.swiperefreshlayout)
|
||||
implementation(libs.androidx.work.runtime.ktx)
|
||||
implementation(libs.androidx.work.rxjava3)
|
||||
implementation(libs.google.android.material)
|
||||
implementation(libs.androidx.webkit)
|
||||
|
||||
/** Compose & other modern patterns **/
|
||||
// Jetpack Compose
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.compose.adaptive)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
implementation(libs.androidx.compose.ui.text) // Needed for parsing HTML to AnnotatedString
|
||||
implementation(libs.androidx.compose.material.icons.extended)
|
||||
|
||||
// Jetpack Compose related dependencies
|
||||
implementation(libs.androidx.paging.compose)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
|
||||
// Coroutines interop
|
||||
implementation(libs.kotlinx.coroutines.rx3)
|
||||
|
||||
// Library loading for About screen
|
||||
implementation(libs.about.libraries.compose.m3)
|
||||
|
||||
// Hilt
|
||||
implementation(libs.hilt.android)
|
||||
ksp(libs.hilt.compiler)
|
||||
|
||||
// Scroll
|
||||
implementation(libs.lazy.column.scrollbar)
|
||||
|
||||
// Kotlinx Serialization
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
|
||||
/** Third-party libraries **/
|
||||
// Instance state boilerplate elimination
|
||||
implementation(libs.livefront.bridge)
|
||||
implementation(libs.evernote.statesaver.core)
|
||||
kapt(libs.evernote.statesaver.compiler)
|
||||
|
||||
// HTML parser
|
||||
implementation(libs.jsoup)
|
||||
|
||||
// HTTP client
|
||||
implementation(libs.squareup.okhttp)
|
||||
|
||||
// Media player
|
||||
implementation(libs.google.exoplayer.core)
|
||||
implementation(libs.google.exoplayer.dash)
|
||||
implementation(libs.google.exoplayer.database)
|
||||
implementation(libs.google.exoplayer.datasource)
|
||||
implementation(libs.google.exoplayer.hls)
|
||||
implementation(libs.google.exoplayer.mediasession)
|
||||
implementation(libs.google.exoplayer.smoothstreaming)
|
||||
implementation(libs.google.exoplayer.ui)
|
||||
|
||||
// Metadata generator for service descriptors
|
||||
compileOnly(libs.google.autoservice.annotations)
|
||||
ksp(libs.google.autoservice.compiler)
|
||||
|
||||
// Manager for complex RecyclerView layouts
|
||||
implementation(libs.lisawray.groupie.core)
|
||||
implementation(libs.lisawray.groupie.viewbinding)
|
||||
|
||||
// Image loading
|
||||
implementation(libs.coil.compose)
|
||||
implementation(libs.coil.network.okhttp)
|
||||
|
||||
// Markdown library for Android
|
||||
implementation(libs.noties.markwon.core)
|
||||
implementation(libs.noties.markwon.linkify)
|
||||
|
||||
// Crash reporting
|
||||
implementation(libs.acra.core)
|
||||
|
||||
// Properly restarting
|
||||
implementation(libs.jakewharton.phoenix)
|
||||
|
||||
// Reactive extensions for Java VM
|
||||
implementation(libs.reactivex.rxjava)
|
||||
implementation(libs.reactivex.rxandroid)
|
||||
// RxJava binding APIs for Android UI widgets
|
||||
implementation(libs.jakewharton.rxbinding)
|
||||
|
||||
// Date and time formatting
|
||||
implementation(libs.ocpsoft.prettytime)
|
||||
|
||||
/** Debugging **/
|
||||
// Memory leak detection
|
||||
debugImplementation(libs.squareup.leakcanary.watcher)
|
||||
debugImplementation(libs.squareup.leakcanary.plumber)
|
||||
debugImplementation(libs.squareup.leakcanary.core)
|
||||
// Debug bridge for Android
|
||||
debugImplementation(libs.facebook.stetho.core)
|
||||
debugImplementation(libs.facebook.stetho.okhttp3)
|
||||
|
||||
// Jetpack Compose
|
||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||
|
||||
/** Testing **/
|
||||
testImplementation(libs.junit)
|
||||
testImplementation(libs.mockito.core)
|
||||
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.runner)
|
||||
androidTestImplementation(libs.androidx.room.testing)
|
||||
androidTestImplementation(libs.assertj.core)
|
||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
||||
debugImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,6 +338,7 @@
|
||||
<data android:scheme="https" />
|
||||
<data android:host="soundcloud.com" />
|
||||
<data android:host="m.soundcloud.com" />
|
||||
<data android:host="on.soundcloud.com" />
|
||||
<data android:host="www.soundcloud.com" />
|
||||
<data android:pathPrefix="/" />
|
||||
</intent-filter>
|
||||
|
||||
@@ -65,6 +65,12 @@ open class App :
|
||||
SingletonImageLoader.Factory {
|
||||
var isFirstRun = false
|
||||
private set
|
||||
var notificationsRequested = false
|
||||
private set
|
||||
|
||||
fun setNotificationsRequested() {
|
||||
notificationsRequested = true
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
super.attachBaseContext(base)
|
||||
|
||||
@@ -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
|
||||
"""
|
||||
)
|
||||
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();
|
||||
}
|
||||
@@ -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,43 @@
|
||||
/*
|
||||
* 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.BasicDAO
|
||||
import org.schabi.newpipe.database.history.model.SearchHistoryEntry
|
||||
|
||||
@Dao
|
||||
interface SearchHistoryDAO : BasicDAO<SearchHistoryEntry> {
|
||||
|
||||
@get:Query("SELECT * FROM search_history WHERE id = (SELECT MAX(id) FROM search_history)")
|
||||
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,61 @@
|
||||
/*
|
||||
* 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.BasicDAO
|
||||
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 : BasicDAO<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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
@@ -23,18 +29,21 @@ data class PlaylistStreamEntry(
|
||||
val joinIndex: Int
|
||||
) : LocalItem {
|
||||
|
||||
override val localItemType: LocalItem.LocalItemType
|
||||
get() = LocalItem.LocalItemType.PLAYLIST_STREAM_ITEM
|
||||
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun toStreamInfoItem(): StreamInfoItem {
|
||||
val item = StreamInfoItem(streamEntity.serviceId, streamEntity.url, streamEntity.title, streamEntity.streamType)
|
||||
item.duration = streamEntity.duration
|
||||
item.uploaderName = streamEntity.uploader
|
||||
item.uploaderUrl = streamEntity.uploaderUrl
|
||||
item.thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl)
|
||||
|
||||
return item
|
||||
}
|
||||
|
||||
override fun getLocalItemType(): LocalItem.LocalItemType {
|
||||
return LocalItem.LocalItemType.PLAYLIST_STREAM_ITEM
|
||||
return StreamInfoItem(
|
||||
streamEntity.serviceId,
|
||||
streamEntity.url,
|
||||
streamEntity.title,
|
||||
streamEntity.streamType
|
||||
).apply {
|
||||
duration = streamEntity.duration
|
||||
uploaderName = streamEntity.uploader
|
||||
uploaderUrl = streamEntity.uploaderUrl
|
||||
thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Ignore
|
||||
import org.schabi.newpipe.database.LocalItem
|
||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity.Companion.STREAM_PROGRESS_MILLIS
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import org.schabi.newpipe.util.image.ImageStrategy
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
class StreamStatisticsEntry(
|
||||
data class StreamStatisticsEntry(
|
||||
@Embedded
|
||||
val streamEntity: StreamEntity,
|
||||
|
||||
@@ -26,18 +33,23 @@ class StreamStatisticsEntry(
|
||||
@ColumnInfo(name = STREAM_WATCH_COUNT)
|
||||
val watchCount: Long
|
||||
) : LocalItem {
|
||||
|
||||
override val localItemType: LocalItem.LocalItemType
|
||||
get() = LocalItem.LocalItemType.STATISTIC_STREAM_ITEM
|
||||
|
||||
@Ignore
|
||||
fun toStreamInfoItem(): StreamInfoItem {
|
||||
val item = StreamInfoItem(streamEntity.serviceId, streamEntity.url, streamEntity.title, streamEntity.streamType)
|
||||
item.duration = streamEntity.duration
|
||||
item.uploaderName = streamEntity.uploader
|
||||
item.uploaderUrl = streamEntity.uploaderUrl
|
||||
item.thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl)
|
||||
|
||||
return item
|
||||
}
|
||||
|
||||
override fun getLocalItemType(): LocalItem.LocalItemType {
|
||||
return LocalItem.LocalItemType.STATISTIC_STREAM_ITEM
|
||||
return StreamInfoItem(
|
||||
streamEntity.serviceId,
|
||||
streamEntity.url,
|
||||
streamEntity.title,
|
||||
streamEntity.streamType
|
||||
).apply {
|
||||
duration = streamEntity.duration
|
||||
uploaderName = streamEntity.uploader
|
||||
uploaderUrl = streamEntity.uploaderUrl
|
||||
thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
package org.schabi.newpipe.database.stream.dao;
|
||||
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
|
||||
|
||||
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 io.reactivex.rxjava3.core.Maybe;
|
||||
|
||||
@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")
|
||||
Maybe<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,46 @@
|
||||
/*
|
||||
* 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 io.reactivex.rxjava3.core.Maybe
|
||||
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): Maybe<StreamStateEntity>
|
||||
|
||||
@Query("DELETE FROM " + StreamStateEntity.STREAM_STATE_TABLE + " WHERE " + StreamStateEntity.JOIN_STREAM_ID + " = :streamId")
|
||||
fun deleteState(streamId: Long): Int
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.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) {
|
||||
entity.uid = uidFromInsert
|
||||
} else {
|
||||
val subscriptionIdFromDb = getSubscriptionIdInternal(entity.serviceId, entity.url)
|
||||
val subscriptionIdFromDb = getSubscriptionIdInternal(entity.serviceId, entity.url!!)
|
||||
?: throw IllegalStateException("Subscription cannot be null just after insertion.")
|
||||
entity.uid = subscriptionIdFromDb
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1561,7 +1561,8 @@ class VideoDetailFragment :
|
||||
.subscribe(
|
||||
{ state -> updatePlaybackProgress(state.progressMillis, info.duration * 1000) },
|
||||
{ throwable -> /* impossible due to the onErrorComplete() */ },
|
||||
{ /* onComplete */
|
||||
{
|
||||
/* onComplete */
|
||||
binding.positionView.visibility = View.GONE
|
||||
binding.detailPositionView.visibility = View.GONE
|
||||
}
|
||||
|
||||
@@ -364,10 +364,10 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
||||
final SubscriptionEntity channel = new SubscriptionEntity();
|
||||
channel.setServiceId(info.getServiceId());
|
||||
channel.setUrl(info.getUrl());
|
||||
channel.setData(info.getName(),
|
||||
ImageStrategy.imageListToDbUrl(info.getAvatars()),
|
||||
info.getDescription(),
|
||||
info.getSubscriberCount());
|
||||
channel.setName(info.getName());
|
||||
channel.setAvatarUrl(ImageStrategy.imageListToDbUrl(info.getAvatars()));
|
||||
channel.setDescription(info.getDescription());
|
||||
channel.setSubscriberCount(info.getSubscriberCount());
|
||||
channelSubscription = null;
|
||||
updateNotifyButton(null);
|
||||
subscribeButtonMonitor = monitorSubscribeButton(mapOnSubscribe(channel));
|
||||
|
||||
@@ -76,7 +76,8 @@ public class SuggestionListAdapter
|
||||
}
|
||||
}
|
||||
|
||||
private static class SuggestionItemCallback extends DiffUtil.ItemCallback<SuggestionItem> {
|
||||
private static final class SuggestionItemCallback
|
||||
extends DiffUtil.ItemCallback<SuggestionItem> {
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull final SuggestionItem oldItem,
|
||||
@NonNull final SuggestionItem newItem) {
|
||||
|
||||
@@ -145,7 +145,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
||||
if (selectedItem instanceof PlaylistMetadataEntry) {
|
||||
final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
|
||||
NavigationHelper.openLocalPlaylistFragment(fragmentManager, entry.getUid(),
|
||||
entry.name);
|
||||
entry.getOrderingName());
|
||||
|
||||
} else if (selectedItem instanceof PlaylistRemoteEntity) {
|
||||
final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem);
|
||||
@@ -153,7 +153,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
||||
fragmentManager,
|
||||
entry.getServiceId(),
|
||||
entry.getUrl(),
|
||||
entry.getName());
|
||||
entry.getOrderingName());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,11 +383,11 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
||||
|
||||
if (item instanceof PlaylistMetadataEntry
|
||||
&& ((PlaylistMetadataEntry) item).getDisplayIndex() != i) {
|
||||
((PlaylistMetadataEntry) item).setDisplayIndex(i);
|
||||
((PlaylistMetadataEntry) item).setDisplayIndex((long) i);
|
||||
localItemsUpdate.add((PlaylistMetadataEntry) item);
|
||||
} else if (item instanceof PlaylistRemoteEntity
|
||||
&& ((PlaylistRemoteEntity) item).getDisplayIndex() != i) {
|
||||
((PlaylistRemoteEntity) item).setDisplayIndex(i);
|
||||
((PlaylistRemoteEntity) item).setDisplayIndex((long) i);
|
||||
remoteItemsUpdate.add((PlaylistRemoteEntity) item);
|
||||
}
|
||||
}
|
||||
@@ -492,7 +492,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void showRemoteDeleteDialog(final PlaylistRemoteEntity item) {
|
||||
showDeleteDialog(item.getName(), item);
|
||||
showDeleteDialog(item.getOrderingName(), item);
|
||||
}
|
||||
|
||||
private void showLocalDialog(final PlaylistMetadataEntry selectedItem) {
|
||||
@@ -513,7 +513,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
||||
if (items.get(index).equals(rename)) {
|
||||
showRenameDialog(selectedItem);
|
||||
} else if (items.get(index).equals(delete)) {
|
||||
showDeleteDialog(selectedItem.name, selectedItem);
|
||||
showDeleteDialog(selectedItem.getOrderingName(), selectedItem);
|
||||
} else if (isThumbnailPermanent && items.get(index).equals(unsetThumbnail)) {
|
||||
final long thumbnailStreamId = localPlaylistManager
|
||||
.getAutomaticPlaylistThumbnailStreamId(selectedItem.getUid());
|
||||
@@ -534,7 +534,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
||||
DialogEditTextBinding.inflate(getLayoutInflater());
|
||||
dialogBinding.dialogEditText.setHint(R.string.name);
|
||||
dialogBinding.dialogEditText.setInputType(InputType.TYPE_CLASS_TEXT);
|
||||
dialogBinding.dialogEditText.setText(selectedItem.name);
|
||||
dialogBinding.dialogEditText.setText(selectedItem.getOrderingName());
|
||||
|
||||
new AlertDialog.Builder(activity)
|
||||
.setView(dialogBinding.getRoot())
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.schabi.newpipe.local.dialog;
|
||||
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.DEFAULT_THUMBNAIL_ID;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@@ -14,7 +16,6 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.local.LocalItemListAdapter;
|
||||
@@ -138,7 +139,7 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
|
||||
private boolean anyPlaylistContainsDuplicates(final List<PlaylistDuplicatesEntry> playlists) {
|
||||
return playlists.stream()
|
||||
.anyMatch(playlist -> playlist.timesStreamIsContained > 0);
|
||||
.anyMatch(playlist -> playlist.getTimesStreamIsContained() > 0);
|
||||
}
|
||||
|
||||
private void onPlaylistSelected(@NonNull final LocalPlaylistManager manager,
|
||||
@@ -146,9 +147,9 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
@NonNull final List<StreamEntity> streams) {
|
||||
|
||||
final String toastText;
|
||||
if (playlist.timesStreamIsContained > 0) {
|
||||
if (playlist.getTimesStreamIsContained() > 0) {
|
||||
toastText = getString(R.string.playlist_add_stream_success_duplicate,
|
||||
playlist.timesStreamIsContained);
|
||||
playlist.getTimesStreamIsContained());
|
||||
} else {
|
||||
toastText = getString(R.string.playlist_add_stream_success);
|
||||
}
|
||||
@@ -160,8 +161,9 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
.subscribe(ignored -> {
|
||||
successToast.show();
|
||||
|
||||
if (playlist.thumbnailUrl != null
|
||||
&& playlist.thumbnailUrl.equals(PlaylistEntity.DEFAULT_THUMBNAIL)) {
|
||||
if (playlist.getThumbnailStreamId() != null
|
||||
&& playlist.getThumbnailStreamId() == DEFAULT_THUMBNAIL_ID
|
||||
) {
|
||||
playlistDisposables.add(manager
|
||||
.changePlaylistThumbnail(playlist.getUid(), streams.get(0).getUid(),
|
||||
false)
|
||||
|
||||
@@ -177,7 +177,7 @@ class FeedDatabaseManager(context: Context) {
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun oldestSubscriptionUpdate(groupId: Long): Flowable<List<OffsetDateTime>> {
|
||||
fun oldestSubscriptionUpdate(groupId: Long): Flowable<List<OffsetDateTime?>> {
|
||||
return when (groupId) {
|
||||
FeedGroupEntity.GROUP_ALL_ID -> feedTable.oldestSubscriptionUpdateFromAll()
|
||||
else -> feedTable.oldestSubscriptionUpdate(groupId)
|
||||
|
||||
@@ -512,7 +512,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
.setTitle(R.string.feed_load_error)
|
||||
.setPositiveButton(R.string.unsubscribe) { _, _ ->
|
||||
SubscriptionManager(requireContext())
|
||||
.deleteSubscription(subscriptionEntity.serviceId, subscriptionEntity.url)
|
||||
.deleteSubscription(subscriptionEntity.serviceId, subscriptionEntity.url!!)
|
||||
.subscribe()
|
||||
handleItemsErrors(nextItemsErrors)
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ class FeedViewModel(
|
||||
feedDatabaseManager.oldestSubscriptionUpdate(groupId),
|
||||
|
||||
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())
|
||||
}
|
||||
)
|
||||
|
||||
@@ -34,15 +34,15 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder {
|
||||
return;
|
||||
}
|
||||
|
||||
itemTitleView.setText(item.name);
|
||||
itemTitleView.setText(item.getOrderingName());
|
||||
itemStreamCountView.setText(Localization.localizeStreamCountMini(
|
||||
itemStreamCountView.getContext(), item.streamCount));
|
||||
itemStreamCountView.getContext(), item.getStreamCount()));
|
||||
itemUploaderView.setVisibility(View.INVISIBLE);
|
||||
|
||||
CoilHelper.INSTANCE.loadPlaylistThumbnail(itemThumbnailView, item.thumbnailUrl);
|
||||
CoilHelper.INSTANCE.loadPlaylistThumbnail(itemThumbnailView, item.getThumbnailUrl());
|
||||
|
||||
if (item instanceof PlaylistDuplicatesEntry
|
||||
&& ((PlaylistDuplicatesEntry) item).timesStreamIsContained > 0) {
|
||||
&& ((PlaylistDuplicatesEntry) item).getTimesStreamIsContained() > 0) {
|
||||
itemView.setAlpha(GRAYED_OUT_ALPHA);
|
||||
} else {
|
||||
itemView.setAlpha(1.0f);
|
||||
|
||||
@@ -33,7 +33,7 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
|
||||
return;
|
||||
}
|
||||
|
||||
itemTitleView.setText(item.getName());
|
||||
itemTitleView.setText(item.getOrderingName());
|
||||
itemStreamCountView.setText(Localization.localizeStreamCountMini(
|
||||
itemStreamCountView.getContext(), item.getStreamCount()));
|
||||
// Here is where the uploader name is set in the bookmarked playlists library
|
||||
|
||||
@@ -148,7 +148,7 @@ public class LocalPlaylistManager {
|
||||
|
||||
public boolean getIsPlaylistThumbnailPermanent(final long playlistId) {
|
||||
return playlistTable.getPlaylist(playlistId).blockingFirst().get(0)
|
||||
.getIsThumbnailPermanent();
|
||||
.isThumbnailPermanent();
|
||||
}
|
||||
|
||||
public long getAutomaticPlaylistThumbnailStreamId(final long playlistId) {
|
||||
@@ -174,7 +174,7 @@ public class LocalPlaylistManager {
|
||||
}
|
||||
if (thumbnailStreamId != THUMBNAIL_ID_LEAVE_UNCHANGED) {
|
||||
playlist.setThumbnailStreamId(thumbnailStreamId);
|
||||
playlist.setIsThumbnailPermanent(isPermanent);
|
||||
playlist.setThumbnailPermanent(isPermanent);
|
||||
}
|
||||
return playlistTable.update(playlist);
|
||||
}).subscribeOn(Schedulers.io());
|
||||
|
||||
@@ -25,7 +25,7 @@ class SubscriptionManager(context: Context) {
|
||||
private val feedDatabaseManager = FeedDatabaseManager(context)
|
||||
|
||||
fun subscriptionTable(): SubscriptionDAO = subscriptionTable
|
||||
fun subscriptions() = subscriptionTable.all
|
||||
fun subscriptions() = subscriptionTable.getAll()
|
||||
|
||||
fun getSubscriptions(
|
||||
currentGroupId: Long = FeedGroupEntity.GROUP_ALL_ID,
|
||||
@@ -43,7 +43,7 @@ class SubscriptionManager(context: Context) {
|
||||
}
|
||||
}
|
||||
showOnlyUngrouped -> subscriptionTable.getSubscriptionsOnlyUngrouped(currentGroupId)
|
||||
else -> subscriptionTable.all
|
||||
else -> subscriptionTable.getAll()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,12 +63,12 @@ class SubscriptionManager(context: Context) {
|
||||
subscriptionTable.getSubscription(info.serviceId, info.url)
|
||||
.flatMapCompletable {
|
||||
Completable.fromRunnable {
|
||||
it.setData(
|
||||
info.name,
|
||||
ImageStrategy.imageListToDbUrl(info.avatars),
|
||||
info.description,
|
||||
info.subscriberCount
|
||||
)
|
||||
it.apply {
|
||||
name = info.name
|
||||
avatarUrl = ImageStrategy.imageListToDbUrl(info.avatars)
|
||||
description = info.description
|
||||
subscriberCount = info.subscriberCount
|
||||
}
|
||||
subscriptionTable.update(it)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,9 +37,9 @@ class SubscriptionExportWorker(
|
||||
val uri = inputData.getString(EXPORT_PATH)!!.toUri()
|
||||
val table = NewPipeDatabase.getInstance(applicationContext).subscriptionDAO()
|
||||
val subscriptions =
|
||||
table.all
|
||||
table.getAll()
|
||||
.awaitFirst()
|
||||
.map { SubscriptionItem(it.serviceId, it.url, it.name) }
|
||||
.map { SubscriptionItem(it.serviceId, it.url ?: "", it.name ?: "") }
|
||||
|
||||
val qty = subscriptions.size
|
||||
val title = applicationContext.resources.getQuantityString(R.plurals.export_subscriptions, qty, qty)
|
||||
|
||||
@@ -306,7 +306,7 @@ class MediaBrowserImpl(
|
||||
}
|
||||
|
||||
private fun populateHistory(): Single<List<MediaBrowserCompat.MediaItem>> {
|
||||
val history = database.streamHistoryDAO().getHistory().firstOrError()
|
||||
val history = database.streamHistoryDAO().history.firstOrError()
|
||||
return history.map { items ->
|
||||
items.map { this.createHistoryMediaItem(it) }
|
||||
}
|
||||
|
||||
@@ -215,7 +215,7 @@ class MediaBrowserPlaybackPreparer(
|
||||
}
|
||||
|
||||
val streamId = path[0].toLong()
|
||||
return database.streamHistoryDAO().getHistory()
|
||||
return database.streamHistoryDAO().history
|
||||
.firstOrError()
|
||||
.map { items ->
|
||||
val infoItems = items
|
||||
|
||||
@@ -289,8 +289,10 @@ public final class MainPlayerUi extends VideoPlayerUi implements View.OnLayoutCh
|
||||
binding.topControls.setClickable(true);
|
||||
binding.topControls.setFocusable(true);
|
||||
|
||||
binding.titleTextView.setVisibility(isFullscreen ? View.VISIBLE : View.GONE);
|
||||
binding.channelTextView.setVisibility(isFullscreen ? View.VISIBLE : View.GONE);
|
||||
binding.metadataView.setVisibility(isFullscreen ? View.VISIBLE : View.GONE);
|
||||
|
||||
// Reset workaround changes from popup player
|
||||
binding.audioTrackTextView.setMaxWidth(Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -960,8 +962,7 @@ public final class MainPlayerUi extends VideoPlayerUi implements View.OnLayoutCh
|
||||
}
|
||||
|
||||
private void setupFullscreenButtons(final boolean fullscreen) {
|
||||
binding.titleTextView.setVisibility(fullscreen ? View.VISIBLE : View.GONE);
|
||||
binding.channelTextView.setVisibility(fullscreen ? View.VISIBLE : View.GONE);
|
||||
binding.metadataView.setVisibility(fullscreen ? View.VISIBLE : View.GONE);
|
||||
binding.playerCloseButton.setVisibility(fullscreen ? View.GONE : View.VISIBLE);
|
||||
setupScreenRotationButton();
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import org.schabi.newpipe.player.Player;
|
||||
import org.schabi.newpipe.player.gesture.BasePlayerGestureListener;
|
||||
import org.schabi.newpipe.player.gesture.PopupPlayerGestureListener;
|
||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
|
||||
public final class PopupPlayerUi extends VideoPlayerUi {
|
||||
private static final String TAG = PopupPlayerUi.class.getSimpleName();
|
||||
@@ -174,6 +175,8 @@ public final class PopupPlayerUi extends VideoPlayerUi {
|
||||
binding.topControls.setClickable(false);
|
||||
binding.topControls.setFocusable(false);
|
||||
binding.bottomControls.bringToFront();
|
||||
// Workaround that UI elements are pushed off screen
|
||||
binding.audioTrackTextView.setMaxWidth(DeviceUtils.dpToPx(48, context));
|
||||
super.setupElementsVisibility();
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,8 @@ import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
|
||||
|
||||
@@ -149,9 +151,9 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
|
||||
}
|
||||
|
||||
private void exportDatabase(final StoredFileHelper file, final Uri exportDataUri) {
|
||||
try {
|
||||
try (ExecutorService executor = Executors.newSingleThreadExecutor()) {
|
||||
//checkpoint before export
|
||||
NewPipeDatabase.checkpoint();
|
||||
executor.submit(NewPipeDatabase::checkpoint).get();
|
||||
|
||||
final SharedPreferences preferences = PreferenceManager
|
||||
.getDefaultSharedPreferences(requireContext());
|
||||
|
||||
@@ -396,7 +396,8 @@ public class PeertubeInstanceListFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
private static class PeertubeInstanceCallback extends DiffUtil.ItemCallback<PeertubeInstance> {
|
||||
private static final class PeertubeInstanceCallback
|
||||
extends DiffUtil.ItemCallback<PeertubeInstance> {
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull final PeertubeInstance oldItem,
|
||||
@NonNull final PeertubeInstance newItem) {
|
||||
|
||||
@@ -179,7 +179,7 @@ public class SelectChannelFragment extends DialogFragment {
|
||||
void onCancel();
|
||||
}
|
||||
|
||||
private class SelectChannelAdapter
|
||||
private final class SelectChannelAdapter
|
||||
extends RecyclerView.Adapter<SelectChannelAdapter.SelectChannelItemHolder> {
|
||||
@NonNull
|
||||
@Override
|
||||
|
||||
@@ -175,7 +175,7 @@ public class SelectFeedGroupFragment extends DialogFragment {
|
||||
void onCancel();
|
||||
}
|
||||
|
||||
private class SelectFeedGroupAdapter
|
||||
private final class SelectFeedGroupAdapter
|
||||
extends RecyclerView.Adapter<SelectFeedGroupAdapter.SelectFeedGroupItemHolder> {
|
||||
@NonNull
|
||||
@Override
|
||||
|
||||
@@ -122,12 +122,12 @@ public class SelectPlaylistFragment extends DialogFragment {
|
||||
|
||||
if (selectedItem instanceof PlaylistMetadataEntry) {
|
||||
final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
|
||||
onSelectedListener.onLocalPlaylistSelected(entry.getUid(), entry.name);
|
||||
onSelectedListener.onLocalPlaylistSelected(entry.getUid(), entry.getOrderingName());
|
||||
|
||||
} else if (selectedItem instanceof PlaylistRemoteEntity) {
|
||||
final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem);
|
||||
onSelectedListener.onRemotePlaylistSelected(
|
||||
entry.getServiceId(), entry.getUrl(), entry.getName());
|
||||
entry.getServiceId(), entry.getUrl(), entry.getOrderingName());
|
||||
}
|
||||
}
|
||||
dismiss();
|
||||
@@ -142,7 +142,7 @@ public class SelectPlaylistFragment extends DialogFragment {
|
||||
void onRemotePlaylistSelected(int serviceId, String url, String name);
|
||||
}
|
||||
|
||||
private class SelectPlaylistAdapter
|
||||
private final class SelectPlaylistAdapter
|
||||
extends RecyclerView.Adapter<SelectPlaylistAdapter.SelectPlaylistItemHolder> {
|
||||
@NonNull
|
||||
@Override
|
||||
@@ -159,11 +159,13 @@ public class SelectPlaylistFragment extends DialogFragment {
|
||||
final PlaylistLocalItem selectedItem = playlists.get(position);
|
||||
|
||||
if (selectedItem instanceof PlaylistMetadataEntry entry) {
|
||||
holder.titleView.setText(entry.name);
|
||||
holder.titleView.setText(entry.getOrderingName());
|
||||
holder.view.setOnClickListener(view -> clickedItem(position));
|
||||
CoilHelper.INSTANCE.loadPlaylistThumbnail(holder.thumbnailView, entry.thumbnailUrl);
|
||||
CoilHelper.INSTANCE.loadPlaylistThumbnail(holder.thumbnailView,
|
||||
entry.getThumbnailUrl());
|
||||
|
||||
} else if (selectedItem instanceof PlaylistRemoteEntity entry) {
|
||||
holder.titleView.setText(entry.getName());
|
||||
holder.titleView.setText(entry.getOrderingName());
|
||||
holder.view.setOnClickListener(view -> clickedItem(position));
|
||||
CoilHelper.INSTANCE.loadPlaylistThumbnail(holder.thumbnailView,
|
||||
entry.getThumbnailUrl());
|
||||
|
||||
@@ -31,7 +31,7 @@ class NotificationModeConfigAdapter(
|
||||
|
||||
fun update(newData: List<SubscriptionEntity>) {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -69,7 +69,8 @@ class PreferenceSearchAdapter
|
||||
}
|
||||
}
|
||||
|
||||
private static class PreferenceCallback extends DiffUtil.ItemCallback<PreferenceSearchItem> {
|
||||
private static final class PreferenceCallback
|
||||
extends DiffUtil.ItemCallback<PreferenceSearchItem> {
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull final PreferenceSearchItem oldItem,
|
||||
@NonNull final PreferenceSearchItem newItem) {
|
||||
|
||||
@@ -15,7 +15,11 @@ import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* @author kapodamy
|
||||
* Converts TTML subtitles to SRT format.
|
||||
*
|
||||
* References:
|
||||
* - TTML 2.0 (W3C): https://www.w3.org/TR/ttml2/
|
||||
* - SRT format: https://en.wikipedia.org/wiki/SubRip
|
||||
*/
|
||||
public class SrtFromTtmlWriter {
|
||||
private static final String NEW_LINE = "\r\n";
|
||||
@@ -59,6 +63,226 @@ public class SrtFromTtmlWriter {
|
||||
out.write(text.getBytes(charset));
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode XML or HTML entities into their actual (literal) characters.
|
||||
*
|
||||
* TTML is XML-based, so text nodes may contain escaped entities
|
||||
* instead of direct characters. For example:
|
||||
*
|
||||
* "&" → "&"
|
||||
* "<" → "<"
|
||||
* ">" → ">"
|
||||
* "	" → "\t" (TAB)
|
||||
* "
" ( ) → "\n" (LINE FEED)
|
||||
*
|
||||
* XML files cannot contain characters like "<", ">", "&" directly,
|
||||
* so they must be represented using their entity-encoded forms.
|
||||
*
|
||||
* Jsoup sometimes leaves nested or encoded entities unresolved
|
||||
* (e.g. inside <p> text nodes in TTML files), so this function
|
||||
* acts as a final “safety net” to ensure all entities are decoded
|
||||
* before further normalization.
|
||||
*
|
||||
* Character representation layers for reference:
|
||||
* - Literal characters: <, >, &
|
||||
* → appear in runtime/output text (e.g. final SRT output)
|
||||
* - Escaped entities: <, >, &
|
||||
* → appear in XML/HTML/TTML source files
|
||||
* - Numeric entities:  , 	, 
|
||||
* → appear mainly in XML/TTML files (also valid in HTML)
|
||||
* for non-printable or special characters
|
||||
* - Unicode escapes: \u00A0 (Java/Unicode internal form)
|
||||
* → appear only in Java source code (NOT valid in XML)
|
||||
*
|
||||
* XML entities include both named (&, <) and numeric
|
||||
* ( ,  ) forms.
|
||||
*
|
||||
* @param encodedEntities The raw text fragment possibly containing
|
||||
* encoded XML entities.
|
||||
* @return A decoded string where all entities are replaced by their
|
||||
* actual (literal) characters.
|
||||
*/
|
||||
private String decodeXmlEntities(final String encodedEntities) {
|
||||
return Parser.unescapeEntities(encodedEntities, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle rare XML entity characters like LF: 
(`\n`),
|
||||
* CR: 
(`\r`) and CRLF: (`\r\n`).
|
||||
*
|
||||
* These are technically valid in TTML (XML allows them)
|
||||
* but unusual in practice, since most TTML line breaks
|
||||
* are represented as <br/> tags instead.
|
||||
* As a defensive approach, we normalize them:
|
||||
*
|
||||
* - Windows (\r\n), macOS (\r), and Unix (\n) → unified SRT NEW_LINE (\r\n)
|
||||
*
|
||||
* Although well-formed TTML normally encodes line breaks
|
||||
* as <br/> tags, some auto-generated or malformed TTML files
|
||||
* may embed literal newline entities (
, 
). This
|
||||
* normalization ensures these cases render properly in SRT
|
||||
* players instead of breaking the subtitle structure.
|
||||
*
|
||||
* @param text To be normalized text with actual characters.
|
||||
* @return Unified SRT NEW_LINE converted from all kinds of line breaks.
|
||||
*/
|
||||
private String normalizeLineBreakForSrt(final String text) {
|
||||
String cleaned = text;
|
||||
|
||||
// NOTE:
|
||||
// The order of newline replacements must NOT change,
|
||||
// or duplicated line breaks (e.g. \r\n → \n\n) will occur.
|
||||
cleaned = cleaned.replace("\r\n", "\n")
|
||||
.replace("\r", "\n");
|
||||
|
||||
cleaned = cleaned.replace("\n", NEW_LINE);
|
||||
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
private String normalizeForSrt(final String actualText) {
|
||||
String cleaned = actualText;
|
||||
|
||||
// Replace NBSP "non-breaking space" (\u00A0) with regular space ' '(\u0020).
|
||||
//
|
||||
// Why:
|
||||
// - Some viewers render NBSP(\u00A0) incorrectly:
|
||||
// * MPlayer 1.5: shown as “??”
|
||||
// * Linux command `cat -A`: displayed as control-like markers
|
||||
// (M-BM-)
|
||||
// * Acode (Android editor): displayed as visible replacement
|
||||
// glyphs (red dots)
|
||||
// - Other viewers show it as a normal space (e.g., VS Code 1.104.0,
|
||||
// vlc 3.0.20, mpv 0.37.0, Totem 43.0)
|
||||
// → Mixed rendering creates inconsistency and may confuse users.
|
||||
//
|
||||
// Details:
|
||||
// - YouTube TTML subtitles use both regular spaces (\u0020)
|
||||
// and non-breaking spaces (\u00A0).
|
||||
// - SRT subtitles only support regular spaces (\u0020),
|
||||
// so \u00A0 may cause display issues.
|
||||
// - \u00A0 and \u0020 are visually identical (i.e., they both
|
||||
// appear as spaces ' '), but they differ in Unicode encoding,
|
||||
// and NBSP (\u00A0) renders differently in different viewers.
|
||||
// - SRT is a plain-text format and does not interpret
|
||||
// "non-breaking" behavior.
|
||||
//
|
||||
// Conclusion:
|
||||
// - Ensure uniform behavior, so replace it to a regular space
|
||||
// without "non-breaking" behavior.
|
||||
//
|
||||
// References:
|
||||
// - Unicode U+00A0 NBSP (Latin-1 Supplement):
|
||||
// https://unicode.org/charts/PDF/U0080.pdf
|
||||
cleaned = cleaned.replace('\u00A0', ' ') // Non-breaking space
|
||||
.replace('\u202F', ' ') // Narrow no-break space
|
||||
.replace('\u205F', ' ') // Medium mathematical space
|
||||
.replace('\u3000', ' ') // Ideographic space
|
||||
// \u2000 ~ \u200A are whitespace characters (e.g.,
|
||||
// en space, em space), replaced with regular space (\u0020).
|
||||
.replaceAll("[\\u2000-\\u200A]", " "); // Whitespace characters
|
||||
|
||||
// \u200B ~ \u200F are a range of non-spacing characters
|
||||
// (e.g., zero-width space, zero-width non-joiner, etc.),
|
||||
// which have no effect in *.SRT files and may cause
|
||||
// display issues.
|
||||
// These characters are invisible to the human eye, and
|
||||
// they still exist in the encoding, so they need to be
|
||||
// removed.
|
||||
// After removal, the actual content becomes completely
|
||||
// empty "", meaning there are no characters left, just
|
||||
// an empty space, which helps avoid formatting issues
|
||||
// in subtitles.
|
||||
cleaned = cleaned.replaceAll("[\\u200B-\\u200F]", ""); // Non-spacing characters
|
||||
|
||||
// Remove control characters (\u0000 ~ \u001F, except
|
||||
// \n, \r, \t).
|
||||
// - These are ASCII C0 control codes (e.g. \u0001 SOH,
|
||||
// \u0008 BS, \u001F US), invisible and irrelevant in
|
||||
// subtitles, may cause square boxes (?) in players.
|
||||
// - Reference:
|
||||
// Unicode Basic Latin (https://unicode.org/charts/PDF/U0000.pdf)
|
||||
// ASCII Control (https://en.wikipedia.org/wiki/ASCII#Control_characters)
|
||||
cleaned = cleaned.replaceAll("[\\u0000-\\u0008\\u000B\\u000C\\u000E-\\u001F]", "");
|
||||
|
||||
// Reasoning:
|
||||
// - subtitle files generally don't require tabs for alignment.
|
||||
// - Tabs can be displayed with varying widths across different
|
||||
// editors or platforms, which may cause display issues.
|
||||
// - Replace it with a single space for consistent display
|
||||
// across different editors or platforms.
|
||||
cleaned = cleaned.replace('\t', ' ');
|
||||
|
||||
cleaned = normalizeLineBreakForSrt(cleaned);
|
||||
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
private String sanitizeFragment(final String raw) {
|
||||
if (null == raw) {
|
||||
return "";
|
||||
}
|
||||
|
||||
final String actualCharacters = decodeXmlEntities(raw);
|
||||
|
||||
final String srtSafeText = normalizeForSrt(actualCharacters);
|
||||
|
||||
return srtSafeText;
|
||||
}
|
||||
|
||||
// Recursively process all child nodes to ensure text inside
|
||||
// nested tags (e.g., <span>) is also extracted.
|
||||
private void traverseChildNodesForNestedTags(final Node parent,
|
||||
final StringBuilder text) {
|
||||
for (final Node child : parent.childNodes()) {
|
||||
extractText(child, text);
|
||||
}
|
||||
}
|
||||
|
||||
// CHECKSTYLE:OFF checkstyle:JavadocStyle
|
||||
// checkstyle does not understand that span tags are inside a code block
|
||||
/**
|
||||
* <p>Recursive method to extract text from all nodes.</p>
|
||||
* <p>
|
||||
* This method processes {@link TextNode}s and {@code <br>} tags,
|
||||
* recursively extracting text from nested tags
|
||||
* (e.g. extracting text from nested {@code <span>} tags).
|
||||
* Newlines are added for {@code <br>} tags.
|
||||
* </p>
|
||||
* @param node the current node to process
|
||||
* @param text the {@link StringBuilder} to append the extracted text to
|
||||
*/
|
||||
// --------------------------------------------------------------------
|
||||
// [INTERNAL NOTE] TTML text layer explanation
|
||||
//
|
||||
// TTML parsing involves multiple text "layers":
|
||||
// 1. Raw XML entities (e.g., <,  ) are decoded by Jsoup.
|
||||
// 2. extractText() works on DOM TextNodes (already parsed strings).
|
||||
// 3. sanitizeFragment() decodes remaining entities and fixes
|
||||
// Unicode quirks.
|
||||
// 4. normalizeForSrt() ensures literal text is safe for SRT output.
|
||||
//
|
||||
// In short:
|
||||
// Jsoup handles XML-level syntax,
|
||||
// our code handles text-level normalization for subtitles.
|
||||
// --------------------------------------------------------------------
|
||||
private void extractText(final Node node, final StringBuilder text) {
|
||||
if (node instanceof TextNode textNode) {
|
||||
String rawTtmlFragment = textNode.getWholeText();
|
||||
String srtContent = sanitizeFragment(rawTtmlFragment);
|
||||
text.append(srtContent);
|
||||
} else if (node instanceof Element element) {
|
||||
// <br> is a self-closing HTML tag used to insert a line break.
|
||||
if (element.tagName().equalsIgnoreCase("br")) {
|
||||
// Add a newline for <br> tags
|
||||
text.append(NEW_LINE);
|
||||
}
|
||||
}
|
||||
|
||||
traverseChildNodesForNestedTags(node, text);
|
||||
}
|
||||
// CHECKSTYLE:ON
|
||||
|
||||
public void build(final SharpStream ttml) throws IOException {
|
||||
/*
|
||||
* TTML parser with BASIC support
|
||||
@@ -79,21 +303,15 @@ public class SrtFromTtmlWriter {
|
||||
final Elements paragraphList = doc.select("body > div > p");
|
||||
|
||||
// check if has frames
|
||||
if (paragraphList.size() < 1) {
|
||||
if (paragraphList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (final Element paragraph : paragraphList) {
|
||||
text.setLength(0);
|
||||
|
||||
for (final Node children : paragraph.childNodes()) {
|
||||
if (children instanceof TextNode) {
|
||||
text.append(((TextNode) children).text());
|
||||
} else if (children instanceof Element
|
||||
&& ((Element) children).tagName().equalsIgnoreCase("br")) {
|
||||
text.append(NEW_LINE);
|
||||
}
|
||||
}
|
||||
// Recursively extract text from all child nodes
|
||||
extractText(paragraph, text);
|
||||
|
||||
if (ignoreEmptyFrames && text.length() < 1) {
|
||||
continue;
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.NO_SERVICE_ID
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@Suppress("ktlint:standard:function-naming")
|
||||
fun StreamInfoItem(
|
||||
serviceId: Int = NO_SERVICE_ID,
|
||||
url: String = "",
|
||||
|
||||
@@ -188,6 +188,7 @@ fun Comment(comment: CommentsInfoItem, onCommentAuthorOpened: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("ktlint:standard:function-naming")
|
||||
fun CommentsInfoItem(
|
||||
serviceId: Int = 1,
|
||||
url: String = "",
|
||||
|
||||
@@ -17,6 +17,7 @@ import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.schabi.newpipe.App;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
|
||||
@@ -89,9 +90,12 @@ public final class PermissionHelper {
|
||||
&& ContextCompat.checkSelfPermission(activity,
|
||||
Manifest.permission.POST_NOTIFICATIONS)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(activity,
|
||||
new String[] {Manifest.permission.POST_NOTIFICATIONS}, requestCode);
|
||||
return false;
|
||||
if (!App.getInstance().getNotificationsRequested()) {
|
||||
ActivityCompat.requestPermissions(activity,
|
||||
new String[]{Manifest.permission.POST_NOTIFICATIONS}, requestCode);
|
||||
App.getInstance().setNotificationsRequested();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -50,6 +50,10 @@ public final class PlayButtonHelper {
|
||||
});
|
||||
|
||||
// long click listener
|
||||
playlistControlBinding.playlistCtrlPlayAllButton.setOnLongClickListener(view -> {
|
||||
NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.MAIN);
|
||||
return true;
|
||||
});
|
||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> {
|
||||
NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.POPUP);
|
||||
return true;
|
||||
|
||||
@@ -109,71 +109,89 @@
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/close"
|
||||
android:focusable="true"
|
||||
android:padding="@dimen/player_main_buttons_padding"
|
||||
android:scaleType="fitXY"
|
||||
android:src="@drawable/ic_close"
|
||||
android:visibility="gone"
|
||||
app:tint="@color/white"
|
||||
android:contentDescription="@string/close"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/metadataView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="top"
|
||||
android:orientation="vertical"
|
||||
tools:ignore="RtlHardcoded">
|
||||
android:orientation="horizontal">
|
||||
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
android:id="@+id/titleTextView"
|
||||
android:layout_width="match_parent"
|
||||
<LinearLayout
|
||||
android:id="@+id/metadataView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="marquee"
|
||||
android:fadingEdge="horizontal"
|
||||
android:marqueeRepeatLimit="marquee_forever"
|
||||
android:scrollHorizontally="true"
|
||||
android:singleLine="true"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold"
|
||||
tools:ignore="RtlHardcoded"
|
||||
tools:text="The Video Title LONG very LONG" />
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_weight="1"
|
||||
android:gravity="top"
|
||||
android:orientation="vertical"
|
||||
tools:ignore="NestedWeights,RtlHardcoded">
|
||||
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
android:id="@+id/channelTextView"
|
||||
android:layout_width="match_parent"
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
android:id="@+id/titleTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="marquee"
|
||||
android:fadingEdge="horizontal"
|
||||
android:marqueeRepeatLimit="marquee_forever"
|
||||
android:scrollHorizontally="true"
|
||||
android:singleLine="true"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold"
|
||||
tools:ignore="RtlHardcoded"
|
||||
tools:text="The Video Title LONG very LONG" />
|
||||
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
android:id="@+id/channelTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="marquee"
|
||||
android:fadingEdge="horizontal"
|
||||
android:marqueeRepeatLimit="marquee_forever"
|
||||
android:scrollHorizontally="true"
|
||||
android:singleLine="true"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp"
|
||||
tools:text="The Video Artist LONG very LONG very Long" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="marquee"
|
||||
android:fadingEdge="horizontal"
|
||||
android:marqueeRepeatLimit="marquee_forever"
|
||||
android:scrollHorizontally="true"
|
||||
android:singleLine="true"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp"
|
||||
tools:text="The Video Artist LONG very LONG very Long" />
|
||||
android:layout_weight="1">
|
||||
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
android:id="@+id/audioTrackTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="35dp"
|
||||
android:layout_gravity="right"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center"
|
||||
android:minWidth="0dp"
|
||||
android:padding="@dimen/player_main_buttons_padding"
|
||||
android:singleLine="true"
|
||||
android:textColor="@android:color/white"
|
||||
android:textStyle="bold"
|
||||
android:visibility="gone"
|
||||
tools:ignore="HardcodedText,RtlHardcoded"
|
||||
tools:text="English (United States) original"
|
||||
tools:visibility="visible" />
|
||||
</FrameLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
android:id="@+id/audioTrackTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="35dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:gravity="center"
|
||||
android:minWidth="0dp"
|
||||
android:padding="@dimen/player_main_buttons_padding"
|
||||
android:textColor="@android:color/white"
|
||||
android:textStyle="bold"
|
||||
android:visibility="gone"
|
||||
tools:ignore="HardcodedText,RtlHardcoded"
|
||||
tools:visibility="visible"
|
||||
tools:text="English (Original)" />
|
||||
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
android:id="@+id/qualityTextView"
|
||||
android:layout_width="wrap_content"
|
||||
@@ -209,6 +227,7 @@
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/open_play_queue"
|
||||
android:focusable="true"
|
||||
android:paddingStart="3dp"
|
||||
android:paddingTop="5dp"
|
||||
@@ -218,7 +237,6 @@
|
||||
android:src="@drawable/ic_list"
|
||||
android:visibility="gone"
|
||||
app:tint="@color/white"
|
||||
android:contentDescription="@string/open_play_queue"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
@@ -228,16 +246,16 @@
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/chapters"
|
||||
android:focusable="true"
|
||||
android:paddingStart="6dp"
|
||||
android:paddingTop="3dp"
|
||||
android:paddingEnd="6dp"
|
||||
android:paddingBottom="3dp"
|
||||
android:paddingTop="3dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_menu_book"
|
||||
android:visibility="gone"
|
||||
app:tint="@color/white"
|
||||
android:contentDescription="@string/chapters"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
@@ -246,12 +264,12 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/more_options"
|
||||
android:focusable="true"
|
||||
android:padding="@dimen/player_main_buttons_padding"
|
||||
android:scaleType="fitXY"
|
||||
android:src="@drawable/ic_expand_more"
|
||||
app:tint="@color/white"
|
||||
android:contentDescription="@string/more_options"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -368,11 +386,11 @@
|
||||
android:layout_height="40dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/toggle_fullscreen"
|
||||
android:focusable="true"
|
||||
android:padding="@dimen/player_main_buttons_padding"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_fullscreen"
|
||||
android:contentDescription="@string/toggle_fullscreen"
|
||||
android:visibility="gone"
|
||||
app:tint="@color/white"
|
||||
tools:ignore="RtlHardcoded"
|
||||
@@ -492,13 +510,13 @@
|
||||
android:layout_marginStart="4dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/toggle_screen_orientation"
|
||||
android:focusable="true"
|
||||
android:nextFocusUp="@id/playbackSeekBar"
|
||||
android:padding="@dimen/player_main_buttons_padding"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_fullscreen"
|
||||
android:visibility="gone"
|
||||
android:contentDescription="@string/toggle_screen_orientation"
|
||||
app:tint="@color/white"
|
||||
tools:ignore="RtlHardcoded"
|
||||
tools:visibility="visible" />
|
||||
@@ -520,10 +538,10 @@
|
||||
android:layout_weight="1"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/previous_stream"
|
||||
android:focusable="true"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_previous"
|
||||
android:contentDescription="@string/previous_stream"
|
||||
app:tint="@color/white" />
|
||||
|
||||
|
||||
@@ -533,9 +551,9 @@
|
||||
android:layout_height="60dp"
|
||||
android:layout_weight="1"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/pause"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_pause"
|
||||
android:contentDescription="@string/pause"
|
||||
app:tint="@color/white" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
@@ -546,10 +564,10 @@
|
||||
android:layout_weight="1"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/next_stream"
|
||||
android:focusable="true"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_next"
|
||||
android:contentDescription="@string/next_stream"
|
||||
app:tint="@color/white" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -596,12 +614,12 @@
|
||||
android:layout_marginLeft="40dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/notification_action_repeat"
|
||||
android:focusable="true"
|
||||
android:padding="10dp"
|
||||
android:scaleType="fitXY"
|
||||
android:src="@drawable/exo_controls_repeat_off"
|
||||
android:tint="?attr/colorAccent"
|
||||
android:contentDescription="@string/notification_action_repeat"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
@@ -612,12 +630,12 @@
|
||||
android:layout_toRightOf="@id/repeatButton"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/notification_action_shuffle"
|
||||
android:focusable="true"
|
||||
android:padding="10dp"
|
||||
android:scaleType="fitXY"
|
||||
android:src="@drawable/ic_shuffle"
|
||||
android:tint="?attr/colorAccent"
|
||||
android:contentDescription="@string/notification_action_shuffle"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
@@ -639,12 +657,12 @@
|
||||
android:layout_toLeftOf="@+id/itemsListClose"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/add_to_playlist"
|
||||
android:focusable="true"
|
||||
android:padding="10dp"
|
||||
android:scaleType="fitXY"
|
||||
android:src="@drawable/ic_playlist_add"
|
||||
android:tint="?attr/colorAccent"
|
||||
android:contentDescription="@string/add_to_playlist"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
|
||||
@@ -884,4 +884,24 @@
|
||||
<string name="search_with_service_name_and_filter">البحث %1$s (%2$s)</string>
|
||||
<string name="migration_info_6_7_title">تمت إزالة صفحة أفضل 50 من SoundCloud</string>
|
||||
<string name="migration_info_6_7_message">أوقفت SoundCloud صفحة أفضل 50 الأصلية. تمت إزالة علامة التبويب المقابلة من صفحتك الرئيسية.</string>
|
||||
<string name="migration_info_7_8_title">تمت إزالة تريندات YouTube المجمعة</string>
|
||||
<string name="migration_info_7_8_message">أوقف YouTube صفحة الترند المدمجة اعتبارًا من 21 يوليو 2025. استبدلت NewPipe صفحة الموضوعات المتداولة الافتراضية بصفحة الموضوعات المتداولة الشائعة مع البث المباشر المتداول.\n\nيمكنك أيضًا تحديد صفحات رائجة مختلفة في \"الإعدادات > المحتوى > محتوى الصفحة الرئيسية\".</string>
|
||||
<string name="trending_gaming">توجهات الألعاب</string>
|
||||
<string name="trending_podcasts">توجهات البث الصوتي</string>
|
||||
<string name="trending_movies">الأفلام والعروض الأكثر رواجاً</string>
|
||||
<string name="trending_music">الموسيقى الرائجة</string>
|
||||
<string name="short_thousand">%s الف</string>
|
||||
<string name="short_million">%s مليون</string>
|
||||
<string name="short_billion">%sمليار</string>
|
||||
<string name="permission_display_over_apps_message">لاستخدام المشغل المنبثق، يرجى تحديد %1$s في قائمة إعدادات اندرويد التالية وتمكين %2$s.</string>
|
||||
<string name="permission_display_over_apps_permission_name">“السماح بالعرض فوق التطبيقات الاخرى”</string>
|
||||
<string name="delete_file">حذف ملف</string>
|
||||
<string name="delete_entry">حذف المدخلات</string>
|
||||
<string name="account_terminated_service_provides_reason">تم إنهاء الحساب\n\n%1$s يقدم هذا السبب: %2$s</string>
|
||||
<string name="entry_deleted">تم حذف المدخلات</string>
|
||||
<string name="player_http_403">تم تلقي خطأ HTTP 403 من الخادم أثناء التشغيل، ويرجح أن يكون السبب هو انتهاء صلاحية عنوان URL للبث أو حظر عنوان IP</string>
|
||||
<string name="player_http_invalid_status">حدث خطأ HTTP %1$s من الخادم أثناء التشغيل</string>
|
||||
<string name="youtube_player_http_403">تم تلقي خطأ HTTP 403 من الخادم أثناء التشغيل، ويرجح أن يكون السبب هو حظر عنوان IP أو مشكلات في إزالة التعتيم عن عنوان URL للبث</string>
|
||||
<string name="sign_in_confirm_not_bot_error">رفض %1$s تقديم البيانات، وطلب تسجيل الدخول للتأكد من أن الطالب ليس روبوتًا.\n\nربما تم حظر عنوان IP الخاص بك مؤقتًا من قبل %1$s، يمكنك الانتظار بعض الوقت أو التبديل إلى عنوان IP مختلف (على سبيل المثال عن طريق تشغيل/إيقاف تشغيل VPN، أو التبديل من WiFi إلى بيانات الهاتف المحمول).</string>
|
||||
<string name="unsupported_content_in_country">هذا المحتوى غير متاح للبلد المحدد حاليًا.\n\nقم بتغيير اختيارك من ”الإعدادات > المحتوى > البلد الافتراضي للمحتوى“.</string>
|
||||
</resources>
|
||||
|
||||
@@ -192,10 +192,10 @@
|
||||
<string name="delete">Sil</string>
|
||||
<string name="no_channel_subscribed_yet">Hələ ki, kanal abunəliyi yoxdur</string>
|
||||
<string name="select_a_channel">Kanal seç</string>
|
||||
<string name="channel_page_summary">Kanal Səhifəsi</string>
|
||||
<string name="channel_page_summary">Kanal səhifəsi</string>
|
||||
<string name="default_kiosk_page_summary">Standart Bölmə</string>
|
||||
<string name="kiosk_page_summary">Kənar Səhifə</string>
|
||||
<string name="blank_page_summary">Boş Səhifə</string>
|
||||
<string name="kiosk_page_summary">Kənar səhifə</string>
|
||||
<string name="blank_page_summary">Boş səhifə</string>
|
||||
<string name="main_page_content_summary">Əsas səhifədə hansı tablar göstərilir</string>
|
||||
<string name="main_page_content">Əsas səhifə məzmunu</string>
|
||||
<string name="updates_setting_description">Yeni versiya mövcud olduqda tətbiq yeniləməsini xatırlatmaq üçün bildiriş göstər</string>
|
||||
@@ -295,7 +295,7 @@
|
||||
<string name="what_happened_headline">Nə baş verdi:</string>
|
||||
<string name="detail_uploader_thumbnail_view_description">Yükləyənin avatar miniatürü</string>
|
||||
<string name="detail_likes_img_view_description">Bəyən</string>
|
||||
<string name="detail_dislikes_img_view_description">Bəyənmə</string>
|
||||
<string name="detail_dislikes_img_view_description">Bəyənməmə</string>
|
||||
<string name="detail_drag_description">Yenidən sıralamaq üçün sürüklə</string>
|
||||
<string name="drawer_header_description">Xidməti dəyiş, hazırda seçilmiş:</string>
|
||||
<string name="no_subscribers">Abunəçi yoxdur</string>
|
||||
@@ -801,4 +801,29 @@
|
||||
<string name="select_a_feed_group">Axın qrupu seçin</string>
|
||||
<string name="no_feed_group_created_yet">Hələ heç bir axın qrupu yaradılmayıb</string>
|
||||
<string name="feed_group_page_summary">Kanal qrupu səhifəsi</string>
|
||||
<string name="search_with_service_name">%1$s axtar</string>
|
||||
<string name="search_with_service_name_and_filter">%1$s (%2$s) axtar</string>
|
||||
<string name="channel_tab_likes">Bəyənmə</string>
|
||||
<string name="migration_info_6_7_title">SoundCloud Top 50 səhifəsi silindi</string>
|
||||
<string name="migration_info_6_7_message">SoundCloud ilk Ən yaxşı 50 siyahısın ləğv etdi. Uyğun səhifə əsas səhifənizdən silindi.</string>
|
||||
<string name="short_thousand">%sMin</string>
|
||||
<string name="short_million">%sMln</string>
|
||||
<string name="short_billion">%sMlrd</string>
|
||||
<string name="migration_info_7_8_title">YouTube birləşmiş trend silindi</string>
|
||||
<string name="migration_info_7_8_message">YouTube 21 iyul 2025-ci il tarixindən birləşmiş trend səhifəsini ləğv etdi. NewPipe ilkin trend səhifəsini trend olan canlı yayımlarla əvəz etdi. \n\nHəmçinin \"Tənzimləmələr > Məzmun > Əsas səhifə məzmunu\" bölməsində müxtəlif trendli səhifələri seçə bilərsiniz.</string>
|
||||
<string name="trending_gaming">Trenddə olan Oyun</string>
|
||||
<string name="trending_podcasts">Trenddə olan podkastlar</string>
|
||||
<string name="trending_movies">Trend film və tamaşalar</string>
|
||||
<string name="trending_music">Trenddə olan musiqilər</string>
|
||||
<string name="permission_display_over_apps_message">Ani oynadıcı istifadə etmək üçün lütfən, aşağıdakı Android tənzimləmələr menyusunda %1$s seçin və %2$s-ı aktivləşdirin.</string>
|
||||
<string name="permission_display_over_apps_permission_name">\"Digər tətbiqlər üzərində göstərməyə icazə verin\"</string>
|
||||
<string name="delete_file">Faylı sil</string>
|
||||
<string name="delete_entry">Girişi silin</string>
|
||||
<string name="entry_deleted">Giriş silindi</string>
|
||||
<string name="account_terminated_service_provides_reason">Hesab ləğv edilib\n\n %1$s bu səbəbi təmin edir: %2$s</string>
|
||||
<string name="player_http_403">Oynadarkən serverdən alınan HTTP xətası 403, çox güman ki, yayım URL-si müddətinin bitməsi və ya IP qadağası ilə bağlıdır</string>
|
||||
<string name="player_http_invalid_status">HTTP xətası %1$s oynadarkən serverdən alındı</string>
|
||||
<string name="youtube_player_http_403">HTTP xətası 403 oynadarkən serverdən alındı, ehtimal ki, IP qadağası və ya yayım URL-nin deobfuscation problemləri ilə bağlıdır</string>
|
||||
<string name="sign_in_confirm_not_bot_error">%1$s sorğuçunun bot olmadığını təsdiqləmək üçün giriş tələb edərək data təmin etməkdən imtina etdi.\n\nIP-niz %1$s tərəfindən müvəqqəti şəkildə qadağan oluna bilər, bir müddət gözləyə və ya başqa IP-yə keçə bilərsiniz (məsələn, VPN-i açıb/qapatmaqla və ya WiFi-dan mobil dataya keçməklə).</string>
|
||||
<string name="unsupported_content_in_country">Bu məzmun hazırda seçilən məzmun ölkəsi üçün əlçatan deyil. \n\nSeçiminizi \"Tənzimləmələr > Məzmun > İlkin məzmun ölkəsi\"- dən dəyişin.</string>
|
||||
</resources>
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
<string name="background_player_playing_toast">Прайграванне ў фонавым рэжыме</string>
|
||||
<string name="popup_playing_toast">Прайграванне ва ўсплывальным акне</string>
|
||||
<string name="content">Кантэнт</string>
|
||||
<string name="show_age_restricted_content_title">Паказваць кантэнт 18+</string>
|
||||
<string name="show_age_restricted_content_title">Кантэнт з ўзроставым абмежаваннем</string>
|
||||
<string name="duration_live">Ужывую</string>
|
||||
<string name="downloads">Спампоўкі</string>
|
||||
<string name="downloads_title">Спампоўкі</string>
|
||||
@@ -106,8 +106,8 @@
|
||||
<string name="notification_channel_name">Апавяшчэнне NewPipe</string>
|
||||
<string name="notification_channel_description">Апавяшчэнні для прайгравальніка NewPipe</string>
|
||||
<string name="unknown_content">[Невядома]</string>
|
||||
<string name="switch_to_background">Перайсці ў фон</string>
|
||||
<string name="switch_to_popup">Перайсці ў акно</string>
|
||||
<string name="switch_to_background">Перайсці ў фонавы рэжым</string>
|
||||
<string name="switch_to_popup">Перайсці ў аконны рэжым</string>
|
||||
<string name="switch_to_main">Перайсці ў галоўнае акно</string>
|
||||
<string name="import_data_title">Імпартаваць даныя</string>
|
||||
<string name="export_data_title">Экспартаваць даныя</string>
|
||||
@@ -427,7 +427,7 @@
|
||||
<string name="unmute">Уключыць гук</string>
|
||||
<string name="mute">Адключыць гук</string>
|
||||
<string name="enqueue_stream">Дадаць у чаргу</string>
|
||||
<string name="enqueued">Даданае ў чаргу</string>
|
||||
<string name="enqueued">Дададзена у чаргу</string>
|
||||
<string name="title_activity_play_queue">Чарга прайгравання</string>
|
||||
<string name="most_liked">Найбольш папулярнае</string>
|
||||
<string name="local">Лакальнае</string>
|
||||
@@ -576,7 +576,7 @@
|
||||
<string name="peertube_instance_url_help">Шукайце серверы, якія вам даспадобы, на %s</string>
|
||||
<string name="show_meta_info_title">Паказваць метаданыя</string>
|
||||
<string name="ignore_hardware_media_buttons_title">Ігнараваць падзеі апаратных медыякнопак</string>
|
||||
<string name="show_age_restricted_content_summary">Паказваць змесціва, магчыма непрыдатнае для дзяцей, таму што яно мае ўзроставыя абмежаванні (напрыклад, 18+)</string>
|
||||
<string name="show_age_restricted_content_summary">Паказваць змесціва, якое можа быць непрыдатным для дзяцей, бо мае ўзроставыя абмежаванні (напрыклад, 18+)</string>
|
||||
<string name="error_report_open_github_notice">Праверце, ці не існуе заяўкі з абмеркаваннем вашай праблемы. Дублікаты марнуюць наш час і праз гэта адцягваецца вырашэнне сапраўдных задач.</string>
|
||||
<string name="error_report_notification_toast">Адбылася памылка, глядзіце апавяшчэнне</string>
|
||||
<string name="crash_the_player">Збой плэера</string>
|
||||
@@ -597,8 +597,8 @@
|
||||
<item quantity="other">%s новых трансляцый</item>
|
||||
</plurals>
|
||||
<string name="comments_tab_description">Каментарыі</string>
|
||||
<string name="enqueue_next_stream">У чаргу далей</string>
|
||||
<string name="enqueued_next">У чарзе наступны</string>
|
||||
<string name="enqueue_next_stream">Дадаць у чаргу наступным</string>
|
||||
<string name="enqueued_next">Дададзена у чаргу (наступным)</string>
|
||||
<string name="loading_stream_details">Загрузка звестак аб стрыме…</string>
|
||||
<string name="processing_may_take_a_moment">Ідзе апрацоўка… Крыху пачакайце</string>
|
||||
<string name="playlist_add_stream_success_duplicate">Дублікат дададзены %d раз(ы)</string>
|
||||
@@ -692,22 +692,20 @@
|
||||
<string name="radio">Радыё</string>
|
||||
<string name="feed_hide_streams_title">Паказваць наступныя патокі</string>
|
||||
<string name="feed_show_hide_streams">Паказаць/схаваць трансляцыі</string>
|
||||
<string name="content_not_supported">Гэты кантэнт яшчэ не падтрымліваецца NewPipe.
|
||||
\n
|
||||
\nСпадзяюся, ён будзе падтрымлівацца ў наступных версіях.</string>
|
||||
<string name="content_not_supported">Гэты кантэнт яшчэ не падтрымліваецца NewPipe.\n\nСпадзяёмся, што падтрымка з\'явіцца ў наступных версіях.</string>
|
||||
<string name="playlist_page_summary">Старонка плэй-ліста</string>
|
||||
<string name="show_thumbnail_title">Паказваць мініяцюру</string>
|
||||
<string name="show_thumbnail_summary">Выкарыстоўваць мініяцюру як фон для экрана блакіроўкі і апавяшчэнняў</string>
|
||||
<string name="no_appropriate_file_manager_message">Для гэтага дзеяння не знойдзены прыдатны файлавы менеджар. \nУсталюйце файлавы менеджар або паспрабуйце адключыць «%s» у наладах спампоўвання</string>
|
||||
<string name="georestricted_content">Гэты кантэнт недаступны ў вашай краіне.</string>
|
||||
<string name="soundcloud_go_plus_content">Гэта трэк SoundCloud Go+, прынамсі ў вашай краіне, таму NewPipe не можа трансляваць ці спампоўваць яго.</string>
|
||||
<string name="private_content">Гэта змесціва з\'яўляецца прыватным, таму NewPipe не можа яго трансляваць або спампоўваць.</string>
|
||||
<string name="youtube_music_premium_content">Гэта відэа даступна толькі для падпісчыкаў YouTube Music Premium, таму NewPipe не можа яго трансляваць або спампоўваць.</string>
|
||||
<string name="soundcloud_go_plus_content">Гэта трэк SoundCloud Go+ (прынамсі ў вашай краіне), таму NewPipe не можа яго прайграць або спампаваць.</string>
|
||||
<string name="private_content">Гэты кантэнт прыватны, таму NewPipe не можа яго прайграць або спампаваць.</string>
|
||||
<string name="youtube_music_premium_content">Гэта відэа даступна толькі для падпісчыкаў YouTube Music Premium, таму NewPipe не можа яго прайграць або спампаваць.</string>
|
||||
<string name="account_terminated">Уліковы запіс спынены</string>
|
||||
<string name="featured">Вартае ўвагі</string>
|
||||
<string name="metadata_privacy_internal">Унутраная</string>
|
||||
<string name="feed_show_watched">Прагледжаныя цалкам</string>
|
||||
<string name="paid_content">Гэты кантэнт даступны толькі для аплачаных карыстальнікаў, таму NewPipe не можа яго трансляваць або спампоўваць.</string>
|
||||
<string name="paid_content">Гэты кантэнт даступны карыстальнікам толькі за плату, таму NewPipe не можа яго прайграць або спампаваць.</string>
|
||||
<string name="feed_use_dedicated_fetch_method_summary">Даступна для некаторых сэрвісаў, звычайна значна хутчэй, але можа перадаваць абмежаваную колькасць элементаў і не ўсю інфармацыю (можа адсутнічаць працягласць, тып элемента, паказчык трансляцыі)</string>
|
||||
<string name="metadata_age_limit">Узроставае абмежаванне</string>
|
||||
<string name="no_appropriate_file_manager_message_android_10">Для гэтага дзеяння не знойдзены прыдатны файлавы менеджар. \nУсталюйце файлавы менеджар, сумяшчальны з Storage Access Framework</string>
|
||||
@@ -729,7 +727,7 @@
|
||||
<string name="prefer_original_audio_title">Аддаваць перавагу арыгінальнаму гуку</string>
|
||||
<string name="prefer_descriptive_audio_title">Аддаваць перавагу апісальнаму гуку</string>
|
||||
<string name="prefer_descriptive_audio_summary">Выбіраць гукавую дарожку з апісаннем для людзей са слабым зрокам, калі яна ёсць</string>
|
||||
<string name="play_queue_audio_track">Аўдыя: %s</string>
|
||||
<string name="play_queue_audio_track">Аўдыядарожка: %s</string>
|
||||
<string name="audio_track">Гукавая дарожка</string>
|
||||
<string name="select_audio_track_external_players">Выберыце гукавую дарожку для знешніх прайгравальнікаў</string>
|
||||
<string name="unknown_audio_track">Невядомая</string>
|
||||
@@ -819,4 +817,18 @@
|
||||
<string name="search_with_service_name">Пошук %1$s</string>
|
||||
<string name="search_with_service_name_and_filter">Пошук %1$s (%2$s)</string>
|
||||
<string name="channel_tab_likes">Спадабалася</string>
|
||||
<string name="permission_display_over_apps_permission_name">«Дазволіць паказ па-над астатнімі праграмамі»</string>
|
||||
<string name="short_thousand">%s тыс.</string>
|
||||
<string name="short_million">%s млн</string>
|
||||
<string name="short_billion">%s млрд</string>
|
||||
<string name="delete_file">Выдаліць файл</string>
|
||||
<string name="delete_entry">Выдаліць запіс</string>
|
||||
<string name="migration_info_6_7_title">Старонка SoundCloud Top 50 выдалена</string>
|
||||
<string name="trending_music">Трэнды – музыка</string>
|
||||
<string name="entry_deleted">Запіс выдалены</string>
|
||||
<string name="trending_gaming">Трэнды – гульні</string>
|
||||
<string name="trending_podcasts">Трэнды – падкасты</string>
|
||||
<string name="trending_movies">Трэнды – фільмы і перадачы</string>
|
||||
<string name="unsupported_content_in_country">Гэты кантэнт недаступны для цяперашняй краіны кантэнту.\n\nЯе можна змяніць праз «Налады > Кантэнт > Прадвызначаная краіна кантэнту».</string>
|
||||
<string name="migration_info_7_8_message">21 ліпеня 2025 года YouTube спыніў падтрымку аб\'яднанай старонкі трэндаў. NewPipe замяніў старонку трэндаў на трэнды трансляцый.\n\nТаксама можна выбраць іншыя старонкі трэндаў праз «Налады > Кантэнт > Змесціва галоўнай старонкі».</string>
|
||||
</resources>
|
||||
|
||||
@@ -163,8 +163,8 @@
|
||||
<string name="just_once">Само веднъж</string>
|
||||
<string name="file">Файл</string>
|
||||
<string name="switch_to_background">Мини във фонов режим</string>
|
||||
<string name="switch_to_popup">Мини към нов прозорец</string>
|
||||
<string name="switch_to_main">Мини в основен режим</string>
|
||||
<string name="switch_to_popup">Мини в нов прозорец</string>
|
||||
<string name="switch_to_main">Мини към основен режим</string>
|
||||
<string name="import_data_title">Внасяне на база данни</string>
|
||||
<string name="export_data_title">Изнасяне на база данни</string>
|
||||
<string name="import_data_summary">Замества текущата ви история, абонаменти, списъци за възпроизвеждане и (по избор) настройки</string>
|
||||
@@ -258,7 +258,7 @@
|
||||
<string name="create_playlist">Нов Плейлист</string>
|
||||
<string name="rename_playlist">Преименувай</string>
|
||||
<string name="name">Име</string>
|
||||
<string name="add_to_playlist">Добави Към Плейлист</string>
|
||||
<string name="add_to_playlist">Добави към плейлист</string>
|
||||
<string name="set_as_playlist_thumbnail">Задай като миниатюра на плейлиста</string>
|
||||
<string name="bookmark_playlist">Миниатюрата на плейлиста е сменена</string>
|
||||
<string name="unbookmark_playlist">Премахни отметката</string>
|
||||
@@ -277,7 +277,7 @@
|
||||
<string name="enable_disposed_exceptions_title">Докладвай за извънредни грешки</string>
|
||||
<string name="import_title">Внасяне</string>
|
||||
<string name="import_from">Внасяне от</string>
|
||||
<string name="export_to">Изнеси в</string>
|
||||
<string name="export_to">Изнасяне във</string>
|
||||
<string name="import_ongoing">Внасяне…</string>
|
||||
<string name="export_ongoing">Изнасяне…</string>
|
||||
<string name="import_file_title">Файл с данни за внасяне</string>
|
||||
@@ -659,7 +659,7 @@
|
||||
<string name="peertube_instance_add_help">Въведете URL адреса на инстанцията</string>
|
||||
<string name="play_queue_audio_track">Аудио: %s</string>
|
||||
<string name="show_channel_details">Покажи информация за канала</string>
|
||||
<string name="playlist_no_uploader">Автоматично генерирани (не е намерен ъплоудер)</string>
|
||||
<string name="playlist_no_uploader">Авто-генерирани (не е намерен ъплоудер)</string>
|
||||
<string name="create_error_notification">Създай известие за грешка</string>
|
||||
<string name="auto_update_check_description">NewPipe може автоматично да проверява за нови версии от време на време и да ви известява при наличие.
|
||||
\nИскате ли да го включите?</string>
|
||||
@@ -814,4 +814,24 @@
|
||||
<string name="channel_tab_likes">Харесвания</string>
|
||||
<string name="migration_info_6_7_title">Страница SoundCloud Top 50 е премахната</string>
|
||||
<string name="migration_info_6_7_message">SoundCloud преустанови оригиналните класации Топ 50. Съответният раздел е премахнат от главната ви страница.</string>
|
||||
<string name="migration_info_7_8_message">YouTube преустанови комбинираната страница с популярни от 21 юли 2025 г. NewPipe замени стандартната страница с популярни с популярни предавания на живо.\n\nМожете също да изберете различни популярни страници в „Настройки > Съдържание > Съдържание на главната страница“.</string>
|
||||
<string name="migration_info_7_8_title">YouTube комбинирани популярни са премахнати</string>
|
||||
<string name="trending_gaming">Популярни игри</string>
|
||||
<string name="trending_podcasts">Популярни подкасти</string>
|
||||
<string name="trending_movies">Популярни филми и сериали</string>
|
||||
<string name="trending_music">Популярна музика</string>
|
||||
<string name="short_thousand">%s хил.</string>
|
||||
<string name="short_million">%s млн.</string>
|
||||
<string name="short_billion">%s млрд.</string>
|
||||
<string name="permission_display_over_apps_message">За да използвате изскачащия плейър, моля, изберете %1$s в следното меню с настройки на Android и активирайте %2$s.</string>
|
||||
<string name="permission_display_over_apps_permission_name">“Разреши показване върху други приложения”</string>
|
||||
<string name="delete_file">Изтриване на файл</string>
|
||||
<string name="delete_entry">Изтриване на запис</string>
|
||||
<string name="entry_deleted">Записът е изтрит</string>
|
||||
<string name="account_terminated_service_provides_reason">Профилът е прекратен\n\n%1$s предоставя тази причина: %2$s</string>
|
||||
<string name="player_http_403">HTTP грешка 403, получена от сървъра по време на възпроизвеждане, вероятно причинена от изтичане на URL адреса за стрийминг или забрана на IP адреса</string>
|
||||
<string name="player_http_invalid_status">HTTP грешка %1$s получена от сървъра по време на възпроизвеждане</string>
|
||||
<string name="youtube_player_http_403">HTTP грешка 403, получена от сървъра по време на възпроизвеждане, вероятно причинена от забрана на IP адреса или проблеми с деобфускацията на URL адреси за стрийминг</string>
|
||||
<string name="sign_in_confirm_not_bot_error">%1$s отказа да предостави данни, като поиска вход, за да потвърди, че заявителят не е бот.\n\nВашият IP адрес може да е временно забранен от %1$s. Можете да изчакате известно време или да превключите към друг IP адрес (например като включите/изключите VPN или като превключите от WiFi към мобилни данни).</string>
|
||||
<string name="unsupported_content_in_country">Това съдържание не е налично за текущо избраната държава на съдържанието.\n\nПроменете избора си от \"Настройки > Съдържание > Държава на съдържанието по подразбиране\".</string>
|
||||
</resources>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<string name="channel_unsubscribed">Otkazana pratnja kanala</string>
|
||||
<string name="subscription_change_failed">Nije moguće promijeniti pratnju</string>
|
||||
<string name="subscription_update_failed">Nije moguće ažurirati pratnju</string>
|
||||
<string name="kore_not_found">Instalirajte nedostajeću Kode aplikaciju\?</string>
|
||||
<string name="kore_not_found">Instalirati nedostajuću Kore aplikaciju?</string>
|
||||
<string name="tab_bookmarks">Obilježeni Popisi</string>
|
||||
<string name="controls_popup_title">Iskačni prozor</string>
|
||||
<string name="tab_choose">Izaberite Podprozor</string>
|
||||
@@ -43,9 +43,9 @@
|
||||
<string name="default_popup_resolution_title">Zadani režim za iskačući prozor</string>
|
||||
<string name="crash_the_player">Prekinite pokretač</string>
|
||||
<string name="show_play_with_kodi_summary">Prikažite postavku da biste video preko KODI medijskog centra video pokrenuli</string>
|
||||
<string name="notification_scale_to_square_image_title">Skalirajte sličicu na 1:1 omjer</string>
|
||||
<string name="notification_scale_to_square_image_title">Izrežite sličicu na omjer slike 1:1</string>
|
||||
<string name="notification_action_0_title">Prvo radno dugme</string>
|
||||
<string name="notification_scale_to_square_image_summary">Skalirajte video sličicu prikazanu u obavijesti sa 16:9 na 1:1 omjer (može uvesti poremećaje)</string>
|
||||
<string name="notification_scale_to_square_image_summary">Izrežite sličicu videa prikazanu u obavještenju sa omjera stranica 16:9 na 1:1</string>
|
||||
<string name="notification_action_1_title">Drugo radno dugme</string>
|
||||
<string name="notification_action_2_title">Treće radno dugme</string>
|
||||
<string name="notification_action_3_title">Četvrto radno dugme</string>
|
||||
@@ -53,7 +53,7 @@
|
||||
<string name="notification_actions_at_most_three">Možete najviše tri radnje odabrati za prikaz u kompaktnom obavještaju!</string>
|
||||
<string name="notification_action_repeat">Ponovi</string>
|
||||
<string name="notification_action_shuffle">Pomiješajte</string>
|
||||
<string name="notification_actions_summary">Uredite svaku radnju obavještenja ispod pri dodiru na nju. Odaberite bar tri od njih za prikaz u kompaktnom obavještenju koristeći potvrdne okvire s desne strane</string>
|
||||
<string name="notification_actions_summary">Uredite svaku radnju obavještenja ispod dodirom na nju. Odaberite do tri od njih koje će biti prikazane u kompaktnom obavještenju pomoću potvrdnih okvira s desne strane.</string>
|
||||
<string name="notification_action_nothing">Ništa</string>
|
||||
<string name="notification_colorize_title">Obojite obavještenje</string>
|
||||
<string name="notification_action_buffering">Učitavanje</string>
|
||||
@@ -62,7 +62,7 @@
|
||||
<string name="play_audio">Zvuk</string>
|
||||
<string name="default_video_format_title">Zadani video format</string>
|
||||
<string name="theme_title">Tema</string>
|
||||
<string name="night_theme_title">Noćna Tema</string>
|
||||
<string name="night_theme_title">Noćna tema</string>
|
||||
<string name="light_theme_title">Svijetla</string>
|
||||
<string name="dark_theme_title">Tamna</string>
|
||||
<string name="popup_remember_size_pos_title">Zapamtite podešavanja za iskočne prozore</string>
|
||||
@@ -114,4 +114,709 @@
|
||||
<string name="play_with_kodi_title">Pokrenite s KODI-jem</string>
|
||||
<string name="seek_duration_title">Vrijeme premotavanja naprijed/nazad</string>
|
||||
<string name="clear_queue_confirmation_description">Aktivni pokretni red će biti zamijenjen</string>
|
||||
</resources>
|
||||
<string name="yes">Da</string>
|
||||
<string name="no">Ne</string>
|
||||
<string name="search_with_service_name">Pretraži %1$s</string>
|
||||
<string name="search_with_service_name_and_filter">Pretraži %1$s (%2$s)</string>
|
||||
<string name="tab_bookmarks_short">Plejliste</string>
|
||||
<string name="notification_actions_summary_android13">Uredite svaku radnju obavještenja ispod dodirom na nju. Prve tri radnje (reprodukcija/pauza, prethodno i sljedeće) postavlja sistem i ne mogu se prilagođavati.</string>
|
||||
<string name="progressive_load_interval_title">Veličina intervala učitavanja reprodukcije</string>
|
||||
<string name="progressive_load_interval_summary">Promijenite veličinu intervala učitavanja progresivnog sadržaja (trenutno %s). Niža vrijednost može ubrzati njihovo početno učitavanje</string>
|
||||
<string name="ignore_hardware_media_buttons_title">Zanemari događaje hardverskih medijskih tipki</string>
|
||||
<string name="ignore_hardware_media_buttons_summary">Korisno, na primjer, ako koristite slušalice s pokvarenim fizičkim tipkama</string>
|
||||
<string name="prefer_original_audio_title">Preferiraj originalni audio</string>
|
||||
<string name="prefer_original_audio_summary">Odaberite originalni audio zapis bez obzira na jezik</string>
|
||||
<string name="prefer_descriptive_audio_title">Preferiraj opisni audio</string>
|
||||
<string name="prefer_descriptive_audio_summary">Odaberite audio zapis s opisima za osobe s oštećenim vidom ako su dostupni</string>
|
||||
<string name="left_gesture_control_summary">Odaberite gestu za lijevu polovinu ekrana igrača</string>
|
||||
<string name="left_gesture_control_title">Radnja lijevog pokreta</string>
|
||||
<string name="right_gesture_control_summary">Odaberite gestu za desnu polovinu ekrana igrača</string>
|
||||
<string name="right_gesture_control_title">Radnja desnog gesta</string>
|
||||
<string name="brightness">Svjetlina</string>
|
||||
<string name="volume">Volumen</string>
|
||||
<string name="none">Nema</string>
|
||||
<string name="show_hold_to_append_title">Prikaži savjet \"Drži za dodavanje u red\"</string>
|
||||
<string name="show_hold_to_append_summary">Prikaži savjet prilikom pritiska na pozadinu ili iskačuće dugme u videu \"Detalji:\"</string>
|
||||
<string name="unsupported_url">Nepodržani URL</string>
|
||||
<string name="unsupported_url_dialog_message">URL nije prepoznat. Otvoriti s drugom aplikacijom?</string>
|
||||
<string name="default_content_country_title">Zadana zemlja sadržaja</string>
|
||||
<string name="content_language_title">Zadani jezik sadržaja</string>
|
||||
<string name="peertube_instance_url_title">PeerTube instance</string>
|
||||
<string name="peertube_instance_url_summary">Odaberite svoje omiljene PeerTube instance</string>
|
||||
<string name="peertube_instance_url_help">Pronađite instance koje vam se sviđaju na %s</string>
|
||||
<string name="peertube_instance_add_title">Dodaj instancu</string>
|
||||
<string name="peertube_instance_add_help">Unesite URL instance</string>
|
||||
<string name="peertube_instance_add_fail">Nije moguće validirati instancu</string>
|
||||
<string name="peertube_instance_add_https_only">Podržani su samo HTTPS URL-ovi</string>
|
||||
<string name="peertube_instance_add_exists">Instanca već postoji</string>
|
||||
<string name="settings_category_player_title">Pokretač</string>
|
||||
<string name="settings_category_player_behavior_title">Ponašanje</string>
|
||||
<string name="settings_category_video_audio_title">Video i audio</string>
|
||||
<string name="settings_category_history_title">Historija i keš memorija</string>
|
||||
<string name="settings_category_appearance_title">Izgled</string>
|
||||
<string name="settings_category_debug_title">Debug</string>
|
||||
<string name="settings_category_updates_title">Nadogradnje</string>
|
||||
<string name="settings_category_player_notification_title">Obavještenje za igrača</string>
|
||||
<string name="settings_category_player_notification_summary">Konfigurišite obavještenja o trenutno reprodukovanom toku</string>
|
||||
<string name="settings_category_backup_restore_title">Sigurnosna kopija i vraćanje</string>
|
||||
<string name="background_player_playing_toast">Reprodukcija u pozadini</string>
|
||||
<string name="popup_playing_toast">Reprodukcija u skočnom modu</string>
|
||||
<string name="content">Sadržaj</string>
|
||||
<string name="show_age_restricted_content_title">Prikaži sadržaj s ograničenjem za uzrast</string>
|
||||
<string name="show_age_restricted_content_summary">Prikaži sadržaj koji je možda neprikladan za djecu jer ima starosno ograničenje (npr. 18+)</string>
|
||||
<string name="youtube_restricted_mode_enabled_title">Uključite \"Ograničeni način rada\" na YouTubeu</string>
|
||||
<string name="youtube_restricted_mode_enabled_summary">YouTube nudi \"Ograničeni način rada\" koji skriva potencijalno sadržaj za odrasle</string>
|
||||
<string name="restricted_video">Ovaj video ima dobno ograničenje.\n\nUključite \"%1$s\" u postavkama ako ga želite pogledati.</string>
|
||||
<string name="restricted_video_no_stream">Ovaj video ima dobno ograničenje. \nZbog novih YouTube pravila o videozapisima s dobnim ograničenjem, NewPipe ne može pristupiti nijednom od svojih video tokova i stoga ga ne može reproducirati.</string>
|
||||
<string name="duration_live">Uživo</string>
|
||||
<string name="downloads">Preuzimanja</string>
|
||||
<string name="downloads_title">Preuzimanja</string>
|
||||
<string name="loading_metadata_title">Učitavanje metapodataka…</string>
|
||||
<string name="error_report_title">Izvještaj o grešci</string>
|
||||
<string name="all">Sve</string>
|
||||
<string name="channels">Kanali</string>
|
||||
<string name="playlists">Plejliste</string>
|
||||
<string name="videos_string">Videozapisi</string>
|
||||
<string name="tracks">Snimke</string>
|
||||
<string name="users">Korisnici</string>
|
||||
<string name="events">Događanja</string>
|
||||
<string name="songs">Pjesme</string>
|
||||
<string name="albums">Albuma</string>
|
||||
<string name="artists">Umjetnici</string>
|
||||
<string name="disabled">Onemogućeno</string>
|
||||
<string name="clear">Rasčisti</string>
|
||||
<string name="best_resolution">Najbolja rezolucija</string>
|
||||
<string name="undo">Poništi</string>
|
||||
<string name="file_deleted">Datoteka je izbrisana</string>
|
||||
<string name="play_all">Reproduciraj sve</string>
|
||||
<string name="always">Uvijek</string>
|
||||
<string name="just_once">Samo jednom</string>
|
||||
<string name="file">Datoteka</string>
|
||||
<string name="notifications">Obavijesti</string>
|
||||
<string name="notification_channel_name">Obavještenje o novoj cijevi</string>
|
||||
<string name="notification_channel_description">Obavještenja za NewPipeovog igrača</string>
|
||||
<string name="app_update_notification_channel_name">Obavještenje o ažuriranju aplikacije</string>
|
||||
<string name="app_update_notification_channel_description">Obavještenja za nove verzije NewPipe-a</string>
|
||||
<string name="hash_channel_name">Obavještenje o hešu videa</string>
|
||||
<string name="hash_channel_description">Obavještenja o napretku heširanja videa</string>
|
||||
<string name="streams_notification_channel_name">Novi tokovi</string>
|
||||
<string name="streams_notification_channel_description">Obavještenja o novim tokovima za pretplatnike</string>
|
||||
<string name="error_report_channel_name">Obavještenje o grešci</string>
|
||||
<string name="error_report_channel_description">Obavještenja za prijavu grešaka</string>
|
||||
<string name="unknown_content">[Nepoznato]</string>
|
||||
<string name="switch_to_background">Prebaci na pozadinu</string>
|
||||
<string name="switch_to_popup">Prebaci na skočni prozor</string>
|
||||
<string name="switch_to_main">Prebaci na glavni</string>
|
||||
<string name="import_data_title">Uvoz baze podataka</string>
|
||||
<string name="export_data_title">Izvoz baze podataka</string>
|
||||
<string name="clear_cookie_title">Obriši reCAPTCHA kolačiće</string>
|
||||
<string name="recaptcha_cookies_cleared">reCAPTCHA kolačići su obrisani</string>
|
||||
<string name="import_data_summary">Zaobilazi vašu trenutnu historiju, pretplate, liste pjesama i (opcionalno) postavke</string>
|
||||
<string name="export_data_summary">Izvoz historije, pretplata, plejlista i postavki</string>
|
||||
<string name="clear_cookie_summary">Obrišite kolačiće koje NewPipe pohranjuje kada riješite reCAPTCHA</string>
|
||||
<string name="clear_views_history_title">Obriši historiju gledanja</string>
|
||||
<string name="clear_views_history_summary">Briše historiju reprodukovanih tokova i pozicije reprodukcije</string>
|
||||
<string name="delete_view_history_alert">Izbrisati cijelu historiju gledanja?</string>
|
||||
<string name="watch_history_deleted">Historija gledanja je izbrisana</string>
|
||||
<string name="clear_playback_states_title">Brisanje pozicija reprodukcije</string>
|
||||
<string name="clear_playback_states_summary">Briše sve pozicije reprodukcije</string>
|
||||
<string name="delete_playback_states_alert">Izbrisati sve pozicije reprodukcije?</string>
|
||||
<string name="watch_history_states_deleted">Pozicije reprodukcije su izbrisane</string>
|
||||
<string name="clear_search_history_title">Obriši historiju pretraživanja</string>
|
||||
<string name="clear_search_history_summary">Briše historiju ključnih riječi pretrage</string>
|
||||
<string name="delete_search_history_alert">Izbrisati cijelu historiju pretraživanja?</string>
|
||||
<string name="search_history_deleted">Historija pretrage je izbrisana</string>
|
||||
<string name="fast_mode">Brzi način rada</string>
|
||||
<string name="main_tabs_position_summary">Pomakni glavni birač kartica na dno</string>
|
||||
<string name="main_tabs_position_title">Položaj glavnih kartica</string>
|
||||
<string name="general_error">Greška</string>
|
||||
<string name="download_to_sdcard_error_title">Vanjska pohrana nije dostupna</string>
|
||||
<string name="download_to_sdcard_error_message">Preuzimanje na eksternu SD karticu nije moguće. Poništiti lokaciju mape za preuzimanje?</string>
|
||||
<string name="network_error">Greška mreže</string>
|
||||
<string name="could_not_load_thumbnails">Nije moguće učitati sve sličice</string>
|
||||
<string name="parsing_error">Nije moguće analizirati web stranicu</string>
|
||||
<string name="content_not_available">Sadržaj nije dostupan</string>
|
||||
<string name="could_not_setup_download_menu">Nije moguće postaviti meni za preuzimanje</string>
|
||||
<string name="app_ui_crash">Aplikacija/korisnički interfejs se srušio/la</string>
|
||||
<string name="player_stream_failure">Nije moguće reproducirati ovaj tok</string>
|
||||
<string name="player_unrecoverable_failure">Došlo je do nepopravljive greške igrača</string>
|
||||
<string name="player_recoverable_failure">Oporavak od greške igrača</string>
|
||||
<string name="external_player_unsupported_link_type">Vanjski playeri ne podržavaju ove vrste linkova</string>
|
||||
<string name="video_streams_empty">Nisu pronađeni video tokovi</string>
|
||||
<string name="audio_streams_empty">Nisu pronađeni audio tokovi</string>
|
||||
<string name="missing_file">Datoteka je premještena ili izbrisana</string>
|
||||
<string name="invalid_directory">Fascikla sa popisima</string>
|
||||
<string name="invalid_source">Nema takve datoteke/izvora sadržaja</string>
|
||||
<string name="invalid_file">Datoteka ne postoji ili nedostaje dozvola za čitanje ili pisanje u nju</string>
|
||||
<string name="file_name_empty_error">Naziv datoteke ne može biti prazan</string>
|
||||
<string name="error_occurred_detail">Došlo je do greške: %1$s</string>
|
||||
<string name="no_streams_available_download">Nema dostupnih tokova za preuzimanje</string>
|
||||
<string name="saved_tabs_invalid_json">Nije moguće pročitati sačuvane kartice, pa se koriste zadane</string>
|
||||
<string name="restore_defaults">Vrati zadane postavke</string>
|
||||
<string name="restore_defaults_confirmation">Želite li vratiti zadane postavke?</string>
|
||||
<string name="permission_display_over_apps">Dozvoli prikaz preko drugih aplikacija</string>
|
||||
<string name="permission_display_over_apps_message">Da biste koristili Popup Player, odaberite %1$s u sljedećem meniju postavki Androida i omogućite %2$s.</string>
|
||||
<string name="permission_display_over_apps_permission_name">\"Dozvoli prikaz preko drugih aplikacija\"</string>
|
||||
<string name="error_report_notification_title">NewPipe je naišao na grešku, dodirnite za prijavu</string>
|
||||
<string name="error_report_notification_toast">Došlo je do greške, pogledajte obavještenje</string>
|
||||
<string name="sorry_string">Žao mi je, to se nije trebalo desiti.</string>
|
||||
<string name="error_report_button_text">Prijavi putem e-pošte</string>
|
||||
<string name="copy_for_github">Kopiraj formatirani izvještaj</string>
|
||||
<string name="error_report_open_issue_button_text">Izvještaj na GitHubu</string>
|
||||
<string name="error_report_open_github_notice">Molimo Vas da provjerite da li već postoji problem koji se odnosi na Vaš pad sistema. Prilikom kreiranja duplikata tiketa, oduzimate nam vrijeme koje bismo mogli posvetiti ispravljanju samog problema.</string>
|
||||
<string name="error_snackbar_message">Izvinite, ali nešto je pošlo po zlu.</string>
|
||||
<string name="error_snackbar_action">Prijavi</string>
|
||||
<string name="what_device_headline">Info:</string>
|
||||
<string name="what_happened_headline">Šta se dogodilo:</string>
|
||||
<string name="info_labels">Šta:\\nZahtjev:\\nJezik sadržaja:\\nZemlja sadržaja:\\nJezik aplikacije:\\nUsluga:\\nVremenska oznaka:\\nPaket:\\nVerzija:\\nVerzija OS-a:</string>
|
||||
<string name="your_comment">Vaš komentar (na engleskom):</string>
|
||||
<string name="error_details_headline">Detalji:</string>
|
||||
<string name="detail_thumbnail_view_description">Reproduciraj video, trajanje:</string>
|
||||
<string name="detail_uploader_thumbnail_view_description">Sličica avatara osobe koja je postavila sliku</string>
|
||||
<string name="detail_likes_img_view_description">Sviđanja</string>
|
||||
<string name="detail_dislikes_img_view_description">Nesviđa mi se</string>
|
||||
<string name="comments_tab_description">Komentari</string>
|
||||
<string name="related_items_tab_description">Povezane stavke</string>
|
||||
<string name="description_tab_description">Opis</string>
|
||||
<string name="search_no_results">Bez rezultata</string>
|
||||
<string name="empty_list_subtitle">Ovdje nema ničega osim cvrčaka</string>
|
||||
<string name="import_subscriptions_hint">Uvoz ili izvoz pretplata iz menija s tri tačke</string>
|
||||
<string name="detail_drag_description">Prevucite da promijenite redoslijed</string>
|
||||
<string name="video">Video</string>
|
||||
<string name="audio">Audio</string>
|
||||
<string name="retry">Pokušaj ponovo</string>
|
||||
<string name="short_thousand">%sK</string>
|
||||
<string name="short_million">%sM</string>
|
||||
<string name="short_billion">%sB</string>
|
||||
<string name="drawer_header_description">Uključi/isključi uslugu, trenutno odabrana:</string>
|
||||
<string name="no_subscribers">Nema pretplatnika</string>
|
||||
<string name="subscribers_count_not_available">Broj pretplatnika nije dostupan</string>
|
||||
<string name="no_views">Nema pregleda</string>
|
||||
<string name="no_one_watching">Niko ne gleda</string>
|
||||
<string name="no_one_listening">Niko ne sluša</string>
|
||||
<string name="no_videos">Nema videozapisa</string>
|
||||
<string name="more_than_100_videos">100+ videa</string>
|
||||
<string name="infinite_videos">∞ videozapisi</string>
|
||||
<string name="no_comments">Nema komentara</string>
|
||||
<string name="comments_are_disabled">Komentari su onemogućeni</string>
|
||||
<string name="no_streams">Nema tokova</string>
|
||||
<string name="no_live_streams">Nema prijenosa uživo</string>
|
||||
<string name="start">Početak</string>
|
||||
<string name="pause">Pauziraj</string>
|
||||
<string name="create">Napravi</string>
|
||||
<string name="delete">Izbriši</string>
|
||||
<string name="delete_file">Izbriši datoteku</string>
|
||||
<string name="delete_entry">Izbriši unos</string>
|
||||
<string name="checksum">Kontrolni zbir</string>
|
||||
<string name="dismiss">Raspusti</string>
|
||||
<string name="rename">Preimenuj</string>
|
||||
<string name="msg_name">Naziv datoteke</string>
|
||||
<string name="msg_threads">Teme</string>
|
||||
<string name="msg_error">Greška</string>
|
||||
<string name="msg_running">Preuzimanje NewPipe-a</string>
|
||||
<string name="msg_running_detail">Dodirnite za detalje</string>
|
||||
<string name="msg_calculating_hash">Izračunavanje heša</string>
|
||||
<string name="msg_wait">Molimo pričekajte…</string>
|
||||
<string name="msg_copied">Kopirano u međuspremnik</string>
|
||||
<string name="msg_failed_to_copy">Kopiranje u međuspremnik nije uspjelo</string>
|
||||
<string name="no_available_dir">Molimo vas da kasnije u postavkama definišete folder za preuzimanje</string>
|
||||
<string name="no_dir_yet">Još nije postavljen folder za preuzimanje, odaberite zadani folder za preuzimanje sada</string>
|
||||
<string name="msg_popup_permission">Ova dozvola je potrebna za \notvaranje u skočnom prozoru</string>
|
||||
<string name="one_item_deleted">1 stavka je izbrisana.</string>
|
||||
<string name="title_activity_recaptcha">reCAPTCHA izazov</string>
|
||||
<string name="subtitle_activity_recaptcha">Pritisnite \"Gotovo\" kada riješite problem</string>
|
||||
<string name="recaptcha_request_toast">Zatražen je reCAPTCHA izazov</string>
|
||||
<string name="recaptcha_solve">Riješi</string>
|
||||
<string name="done">Gotovo</string>
|
||||
<string name="settings_category_downloads_title">Preuzimanje</string>
|
||||
<string name="settings_file_charset_title">Dozvoljeni znakovi u nazivima datoteka</string>
|
||||
<string name="settings_file_replacement_character_summary">Nevažeći znakovi se zamjenjuju ovom vrijednošću</string>
|
||||
<string name="settings_file_replacement_character_title">Zamjenski lik</string>
|
||||
<string name="charset_letters_and_digits">Slova i brojevi</string>
|
||||
<string name="charset_most_special_characters">Većina specijalnih znakova</string>
|
||||
<string name="title_activity_about">O NewPipe-u</string>
|
||||
<string name="title_licenses">Licence trećih strana</string>
|
||||
<string name="copyright">© %1$s od %2$s pod %3$s</string>
|
||||
<string name="tab_licenses">Dozvole</string>
|
||||
<string name="app_description">Besplatno lagano tokanje na Androidu.</string>
|
||||
<string name="contribution_title">Doprinesite</string>
|
||||
<string name="contribution_encouragement">Bez obzira da li imate ideje za: prevod, promjene dizajna, čišćenje koda ili zaista velike promjene koda - pomoć je uvijek dobrodošla. Što se više uradi, to bolje postaje!</string>
|
||||
<string name="view_on_github">Pogledajte na GitHubu</string>
|
||||
<string name="donation_title">Donirajte</string>
|
||||
<string name="donation_encouragement">NewPipe je razvijen od strane volontera koji svoje slobodno vrijeme provode pružajući vam najbolje korisničko iskustvo. Doprinesite programerima kako biste ih učinili još boljim dok uživaju u šoljici kafe.</string>
|
||||
<string name="give_back">Vratite</string>
|
||||
<string name="website_title">Web stranica</string>
|
||||
<string name="website_encouragement">Posjetite web stranicu NewPipe za više informacija i novosti.</string>
|
||||
<string name="privacy_policy_title">Politika privatnosti kompanije NewPipe</string>
|
||||
<string name="privacy_policy_encouragement">Projekat NewPipe veoma ozbiljno shvata vašu privatnost. Stoga aplikacija ne prikuplja nikakve podatke bez vašeg pristanka.\nPolitika privatnosti NewPipe-a detaljno objašnjava koji se podaci šalju i pohranjuju kada pošaljete izvještaj o padu sistema.</string>
|
||||
<string name="read_privacy_policy">Pročitajte politiku privatnosti</string>
|
||||
<string name="app_license_title">NewPipe-ova licenca</string>
|
||||
<string name="app_license">NewPipe je copyleft libre softver: Možete ga koristiti, proučavati, dijeliti i poboljšavati po volji. Konkretno, možete ga redistribuirati i/ili mijenjati pod uvjetima GNU Opće javne licence koju je objavila Fondacija za slobodni softver, bilo verzije 3 Licence ili (po vašem izboru) bilo koje kasnije verzije.</string>
|
||||
<string name="read_full_license">Pročitaj licencu</string>
|
||||
<string name="faq_title">Često postavljana pitanja</string>
|
||||
<string name="faq_description">Ako imate problema s korištenjem aplikacije, obavezno pogledajte ove odgovore na česta pitanja!</string>
|
||||
<string name="faq">Pogledajte na web stranici</string>
|
||||
<string name="title_activity_history">Historija</string>
|
||||
<string name="action_history">Historija</string>
|
||||
<string name="delete_item_search_history">Želite li izbrisati ovu stavku iz historije pretrage?</string>
|
||||
<string name="title_last_played">Posljednje igrano</string>
|
||||
<string name="title_most_played">Najigranije</string>
|
||||
<string name="main_page_content">Sadržaj glavne stranice</string>
|
||||
<string name="main_page_content_summary">Koje kartice se prikazuju na glavnoj stranici</string>
|
||||
<string name="main_page_content_swipe_remove">Prevucite stavke da biste ih uklonili</string>
|
||||
<string name="blank_page_summary">Prazna stranica</string>
|
||||
<string name="kiosk_page_summary">Stranica kioska</string>
|
||||
<string name="default_kiosk_page_summary">Zadani kiosk</string>
|
||||
<string name="channel_page_summary">Stranica kanala</string>
|
||||
<string name="select_a_channel">Odaberite kanal</string>
|
||||
<string name="no_channel_subscribed_yet">Još nema pretplata na kanale</string>
|
||||
<string name="select_a_playlist">Odaberite listu za reprodukciju</string>
|
||||
<string name="no_playlist_bookmarked_yet">Još nema oznaka za plejlistu</string>
|
||||
<string name="select_a_kiosk">Odaberite kiosk</string>
|
||||
<string name="export_complete_toast">Izvezeno</string>
|
||||
<string name="import_complete_toast">Uvezeno</string>
|
||||
<string name="no_valid_zip_file">Nema važeće ZIP datoteke</string>
|
||||
<string name="could_not_import_all_files">Upozorenje: Nije moguće uvesti sve datoteke.</string>
|
||||
<string name="override_current_data">Ovo će poništiti vašu trenutnu postavku.</string>
|
||||
<string name="import_settings">Želite li uvesti i postavke?</string>
|
||||
<string name="error_unable_to_load_comments">Nije moguće učitati komentare</string>
|
||||
<string name="select_a_feed_group">Odaberite grupu feedova</string>
|
||||
<string name="no_feed_group_created_yet">Još nije kreirana nijedna grupa feedova</string>
|
||||
<string name="trending">U trendu</string>
|
||||
<string name="top_50">Top 50</string>
|
||||
<string name="new_and_hot">Novo i popularno</string>
|
||||
<string name="local">Lokalno</string>
|
||||
<string name="recently_added">Nedavno dodano</string>
|
||||
<string name="most_liked">Najpopularnije</string>
|
||||
<string name="conferences">Konferencije</string>
|
||||
<string name="title_activity_play_queue">Red za reprodukciju</string>
|
||||
<string name="play_queue_remove">Ukloni</string>
|
||||
<string name="play_queue_stream_detail">Detalji</string>
|
||||
<string name="play_queue_audio_settings">Postavke zvuka</string>
|
||||
<string name="play_queue_audio_track">Audio: %s</string>
|
||||
<string name="audio_track">Zvučni zapis</string>
|
||||
<string name="hold_to_append">Držite za dodavanje u red</string>
|
||||
<string name="show_channel_details">Prikaži detalje kanala</string>
|
||||
<string name="enqueue_stream">Stavi u red</string>
|
||||
<string name="enqueued">Stavljeno u red čekanja</string>
|
||||
<string name="enqueue_next_stream">Stavi sljedeće u red</string>
|
||||
<string name="enqueued_next">Sljedeće u redu čekanja</string>
|
||||
<string name="start_here_on_background">Počni reprodukciju u pozadini</string>
|
||||
<string name="start_here_on_popup">Počnite igrati u iskačućem prozoru</string>
|
||||
<string name="loading_stream_details">Učitavanje detalja toka…</string>
|
||||
<string name="drawer_open">Otvori ladicu</string>
|
||||
<string name="drawer_close">Zatvori ladicu</string>
|
||||
<string name="preferred_open_action_settings_title">Preferirana akcija \'otvaranja\'</string>
|
||||
<string name="preferred_open_action_settings_summary">Zadana radnja pri otvaranju sadržaja — %s</string>
|
||||
<string name="video_player">Video plejer</string>
|
||||
<string name="background_player">Pozadinski plejer</string>
|
||||
<string name="popup_player">Iskačući plejer</string>
|
||||
<string name="always_ask_open_action">Uvijek pitajte</string>
|
||||
<string name="preferred_player_fetcher_notification_title">Dobijanje informacija…</string>
|
||||
<string name="preferred_player_fetcher_notification_message">Učitavanje traženog sadržaja</string>
|
||||
<string name="create_playlist">Nova plejlista</string>
|
||||
<string name="duplicate_in_playlist">Liste za reprodukciju koje su sive već sadrže ovu stavku.</string>
|
||||
<string name="rename_playlist">Preimenuj</string>
|
||||
<string name="name">Ime</string>
|
||||
<string name="add_to_playlist">Dodaj na listu pjesama</string>
|
||||
<string name="processing_may_take_a_moment">Obrada… Može potrajati trenutak</string>
|
||||
<string name="mute">Isključi zvuk</string>
|
||||
<string name="unmute">Uključi zvuk</string>
|
||||
<string name="set_as_playlist_thumbnail">Postavi kao sličicu za reprodukciju</string>
|
||||
<string name="unset_playlist_thumbnail">Poništi trajnu sličicu</string>
|
||||
<string name="bookmark_playlist">Označi plejlistu</string>
|
||||
<string name="unbookmark_playlist">Ukloni oznaku</string>
|
||||
<string name="delete_playlist_prompt">Izbrisati ovaj popis?</string>
|
||||
<string name="playlist_creation_success">Plejlista je kreirana</string>
|
||||
<string name="playlist_add_stream_success">Plejlista</string>
|
||||
<string name="playlist_add_stream_success_duplicate">Duplikat dodan %d puta</string>
|
||||
<string name="playlist_thumbnail_change_success">Sličica plejliste je promijenjena.</string>
|
||||
<string name="playlist_no_uploader">Automatski generirano (nije pronađen korisnik koji je otpremio)</string>
|
||||
<string name="caption_none">Nema titlova</string>
|
||||
<string name="resize_fit">Prilagođeno</string>
|
||||
<string name="resize_fill">Popuni</string>
|
||||
<string name="resize_zoom">Uvećanje</string>
|
||||
<string name="caption_auto_generated">Automatski generirano</string>
|
||||
<string name="caption_setting_title">Titlovi</string>
|
||||
<string name="caption_setting_description">Izmijenite veličinu teksta titlova i stilove pozadine za player. Za primjenu je potrebno ponovno pokretanje aplikacije</string>
|
||||
<string name="leak_canary_not_available">LeakCanary nije dostupan</string>
|
||||
<string name="enable_leak_canary_summary">Praćenje curenja memorije može uzrokovati da aplikacija prestane reagirati prilikom ispisa heap memorije</string>
|
||||
<string name="show_memory_leaks">Prikaži curenje memorije</string>
|
||||
<string name="enable_disposed_exceptions_title">Prijavi greške izvan životnog ciklusa</string>
|
||||
<string name="enable_disposed_exceptions_summary">Prisilno prijavljivanje izuzetaka neisporučenih Rx zahtjeva izvan životnog ciklusa fragmenta ili aktivnosti nakon odlaganja</string>
|
||||
<string name="show_original_time_ago_title">Prikaži originalno vrijeme prije stavki</string>
|
||||
<string name="show_original_time_ago_summary">Originalni tekstovi iz usluga bit će vidljivi u stavkama toka</string>
|
||||
<string name="disable_media_tunneling_title">Onemogući tuneliranje medija</string>
|
||||
<string name="disable_media_tunneling_summary">Onemogućite tuneliranje medija ako se pojavi crni ekran ili se prilikom reprodukcije videa pojavi prekid.</string>
|
||||
<string name="disable_media_tunneling_automatic_info">Tuneliranje medija je onemogućeno prema zadanim postavkama na vašem uređaju jer je poznato da vaš model uređaja to ne podržava.</string>
|
||||
<string name="show_image_indicators_title">Prikaži indikatore slike</string>
|
||||
<string name="show_image_indicators_summary">Prikažite Picasso obojene trake preko slika koje označavaju njihov izvor: crvena za mrežu, plava za disk i zelena za memoriju</string>
|
||||
<string name="show_crash_the_player_title">Prikaži \"Sruši plejer\"</string>
|
||||
<string name="show_crash_the_player_summary">Prikazuje opciju pada sistema prilikom korištenja plejera</string>
|
||||
<string name="check_new_streams">Pokreni provjeru za nove tokove</string>
|
||||
<string name="crash_the_app">Sruši aplikaciju</string>
|
||||
<string name="show_error_snackbar">Prikaži traku s upozorenjem o grešci</string>
|
||||
<string name="create_error_notification">Kreiraj obavještenje o grešci</string>
|
||||
<string name="import_title">Uvoz</string>
|
||||
<string name="import_from">Uvoz iz</string>
|
||||
<string name="export_to">Izvoz u</string>
|
||||
<string name="import_ongoing">Uvoz…</string>
|
||||
<string name="export_ongoing">Izvoz…</string>
|
||||
<string name="import_file_title">Uvoz datoteke</string>
|
||||
<string name="previous_export">Prethodni izvoz</string>
|
||||
<string name="subscriptions_import_unsuccessful">Nije moguće uvesti pretplate</string>
|
||||
<string name="subscriptions_export_unsuccessful">Nije moguće izvesti pretplate</string>
|
||||
<string name="import_youtube_instructions">Uvoz YouTube pretplata iz Google arhive:\n\n1. Idite na ovaj URL: %1$s\n2. Prijavite se kada se to od vas zatraži\n3. Kliknite na \"Svi podaci uključeni\", zatim na \"Poništi odabir svih\", a zatim odaberite samo \"pretplate\" i kliknite na \"U redu\"\n4. Kliknite na \"Sljedeći korak\", a zatim na \"Kreiraj izvoz\"\n5. Kliknite na dugme \"Preuzmi\" nakon što se pojavi\n6. Kliknite na UVOZ DATOTEKE ispod i odaberite preuzetu .zip datoteku\n7. [Ako uvoz .zip datoteke ne uspije] Izvucite .csv datoteku (obično pod \"YouTube i YouTube Music/pretplate/pretplate.csv\"), kliknite na UVOZ DATOTEKE ispod i odaberite izvučenu csv datoteku</string>
|
||||
<string name="import_soundcloud_instructions">Uvezite SoundCloud profil unosom URL-a ili vašeg ID-a:\n\n1. Omogućite \"desktop mode\" u web pregledniku (stranica nije dostupna za mobilne uređaje)\n2. Idite na ovaj URL: %1$s\n3. Prijavite se kada se to od vas zatraži\n4. Kopirajte URL profila na koji ste preusmjereni.</string>
|
||||
<string name="import_soundcloud_instructions_hint">tvoj ID, soundcloud.com/tvojID</string>
|
||||
<string name="import_network_expensive_warning">Imajte na umu da ova operacija može biti skupa za mrežu.\n\nŽelite li nastaviti?</string>
|
||||
<string name="playback_speed_control">Kontrole brzine reprodukcije</string>
|
||||
<string name="playback_tempo">Brzina</string>
|
||||
<string name="playback_pitch">Točka glasa</string>
|
||||
<string name="unhook_checkbox">Otkačite (može uzrokovati distorziju)</string>
|
||||
<string name="skip_silence_checkbox">Premotavanje unaprijed tokom tišine</string>
|
||||
<string name="playback_step">Korak</string>
|
||||
<string name="playback_reset">Resetuj</string>
|
||||
<string name="percent">Postotak</string>
|
||||
<string name="semitone">Poluton</string>
|
||||
<string name="start_accept_privacy_policy">Kako bismo se pridržavali Opće uredbe o zaštiti podataka (GDPR), ovim putem skrećemo vašu pažnju na politiku privatnosti kompanije NewPipe. Molimo vas da je pažljivo pročitate.\nMorate je prihvatiti da biste nam poslali izvještaj o grešci.</string>
|
||||
<string name="accept">Prihvati</string>
|
||||
<string name="decline">Odbij</string>
|
||||
<string name="limit_data_usage_none_description">Bez ograničenja</string>
|
||||
<string name="limit_mobile_data_usage_title">Ograničenje rezolucije prilikom korištenja mobilnih podataka</string>
|
||||
<string name="enable_streams_notifications_title">Obavještenja o novim tokovima</string>
|
||||
<string name="enable_streams_notifications_summary">Obavijesti me o novim tokovima s pretplata</string>
|
||||
<string name="streams_notifications_interval_title">Učestalost provjere</string>
|
||||
<string name="streams_notifications_network_title">Potrebna mrežna veza</string>
|
||||
<string name="any_network">Bilo koja mreža</string>
|
||||
<string name="updates_setting_title">Nadogradnje</string>
|
||||
<string name="updates_setting_description">Prikaži obavještenje za podsticanje ažuriranja aplikacije kada je dostupna nova verzija</string>
|
||||
<string name="check_for_updates">Provjeri ažuriranja</string>
|
||||
<string name="auto_update_check_description">NewPipe može automatski provjeravati nove verzije s vremena na vrijeme i obavijestiti vas kada budu dostupne.\nŽelite li ovo omogućiti?</string>
|
||||
<string name="manual_update_description">Ručno provjerite nove verzije</string>
|
||||
<string name="minimize_on_exit_title">Minimiziraj pri prebacivanju aplikacija</string>
|
||||
<string name="minimize_on_exit_summary">Radnja prilikom prelaska na drugu aplikaciju iz glavnog video plejera — %s</string>
|
||||
<string name="minimize_on_exit_none_description">Nema</string>
|
||||
<string name="minimize_on_exit_background_description">Minimiziraj na pozadinski plejer</string>
|
||||
<string name="minimize_on_exit_popup_description">Minimiziraj da bi se player pojavio u skočnom prozoru</string>
|
||||
<string name="autoplay_summary">Automatski pokreni reprodukciju — %s</string>
|
||||
<string name="wifi_only">Samo na Wi-Fi mreži</string>
|
||||
<string name="never">Nikad</string>
|
||||
<string name="list_view_mode">Način prikaza liste</string>
|
||||
<string name="list">Spisak</string>
|
||||
<string name="grid">Rešetka</string>
|
||||
<string name="card">Kartica</string>
|
||||
<string name="auto">Automatski</string>
|
||||
<string name="seekbar_preview_thumbnail_title">Pregled sličice trake za pretraživanje</string>
|
||||
<string name="high_quality_larger">Visok kvalitet (veći)</string>
|
||||
<string name="low_quality_smaller">Nizak kvalitet (manji)</string>
|
||||
<string name="dont_show">Ne prikazuj</string>
|
||||
<string name="app_update_unavailable_toast">Koristite najnoviju verziju NewPipe-a</string>
|
||||
<string name="app_update_available_notification_title">Ažuriranje NewPipe-a je dostupno!</string>
|
||||
<string name="app_update_available_notification_text">Dodirnite za preuzimanje %s</string>
|
||||
<string name="missions_header_finished">Završeno</string>
|
||||
<string name="missions_header_pending">Na čekanju</string>
|
||||
<string name="paused">pauzirano</string>
|
||||
<string name="queued">u redu čekanja</string>
|
||||
<string name="post_processing">naknadna obrada</string>
|
||||
<string name="recovering">oporavlja se</string>
|
||||
<string name="enqueue">Stavi u red</string>
|
||||
<string name="permission_denied">Sistem je odbio akciju</string>
|
||||
<string name="checking_updates_toast">Provjera ažuriranja…</string>
|
||||
<string name="download_failed">Preuzimanje nije uspjelo</string>
|
||||
<string name="reset_settings_title">Resetiraj postavke</string>
|
||||
<string name="reset_settings_summary">Resetujte sve postavke na njihove zadane vrijednosti</string>
|
||||
<string name="reset_all_settings">Resetovanjem svih postavki poništit ćete sve svoje željene postavke i ponovo pokrenuti aplikaciju.\n\nJeste li sigurni da želite nastaviti?</string>
|
||||
<string name="generate_unique_name">Generiraj jedinstveno ime</string>
|
||||
<string name="overwrite">Prebriši</string>
|
||||
<string name="overwrite_unrelated_warning">Datoteka s ovim imenom već postoji</string>
|
||||
<string name="overwrite_finished_warning">Preuzeta datoteka s ovim nazivom već postoji</string>
|
||||
<string name="overwrite_failed">ne može prepisati datoteku</string>
|
||||
<string name="download_already_running">U toku je preuzimanje s ovim imenom</string>
|
||||
<string name="download_already_pending">Postoji preuzimanje s ovim nazivom na čekanju</string>
|
||||
<string name="show_error">Prikaži grešku</string>
|
||||
<string name="error_file_creation">Datoteka ne može biti kreirana</string>
|
||||
<string name="error_path_creation">Nije moguće kreirati odredišnu mapu</string>
|
||||
<string name="error_ssl_exception">Nije moguće uspostaviti sigurnu vezu</string>
|
||||
<string name="error_unknown_host">Nije moguće pronaći server</string>
|
||||
<string name="error_connect_host">Ne mogu se povezati sa serverom</string>
|
||||
<string name="error_http_no_content">Server ne šalje podatke</string>
|
||||
<string name="error_http_unsupported_range">Server ne prihvata višenitna preuzimanja, pokušajte ponovo sa @string/msg_threads = 1</string>
|
||||
<string name="error_http_not_found">Nije pronađeno</string>
|
||||
<string name="error_postprocessing_failed">Naknadna obrada nije uspjela</string>
|
||||
<string name="error_postprocessing_stopped">NewPipe je zatvoren tokom rada na datoteci</string>
|
||||
<string name="error_insufficient_storage">Nema dovoljno slobodnog prostora na uređaju</string>
|
||||
<string name="error_insufficient_storage_left">Nema više prostora na uređaju</string>
|
||||
<string name="error_progress_lost">Napredak je izgubljen jer je datoteka izbrisana</string>
|
||||
<string name="error_timeout">Vremensko ograničenje veze</string>
|
||||
<string name="error_download_resource_gone">Nije moguće oporaviti ovo preuzimanje</string>
|
||||
<string name="clear_download_history">Obriši historiju preuzimanja</string>
|
||||
<string name="confirm_prompt">Želite li obrisati historiju preuzimanja ili izbrisati sve preuzete datoteke?</string>
|
||||
<string name="delete_downloaded_files">Izbriši preuzete datoteke</string>
|
||||
<string name="delete_downloaded_files_confirm">Izbrisati sve preuzete datoteke s diska?</string>
|
||||
<string name="stop">Zaustavi</string>
|
||||
<string name="max_retry_msg">Maksimalan broj ponovnih pokušaja</string>
|
||||
<string name="max_retry_desc">Maksimalan broj pokušaja prije otkazivanja preuzimanja</string>
|
||||
<string name="pause_downloads_on_mobile">Prekid na mrežama s ograničenim pristupom</string>
|
||||
<string name="pause_downloads_on_mobile_desc">Korisno prilikom prelaska na mobilne podatke, iako se neka preuzimanja ne mogu obustaviti</string>
|
||||
<string name="close">Zatvori</string>
|
||||
<string name="enable_queue_limit">Ograniči red čekanja za preuzimanje</string>
|
||||
<string name="enable_queue_limit_desc">Jedno preuzimanje će se pokrenuti istovremeno</string>
|
||||
<string name="start_downloads">Započni preuzimanja</string>
|
||||
<string name="pause_downloads">Pauziraj preuzimanja</string>
|
||||
<string name="downloads_storage_ask_title">Pitaj gdje preuzeti</string>
|
||||
<string name="downloads_storage_ask_summary">Bit ćete upitani gdje želite sačuvati svako preuzimanje.\nOmogućite birač sistemskih foldera (SAF) ako želite preuzeti na eksternu SD karticu</string>
|
||||
<string name="downloads_storage_ask_summary_no_saf_notice">Bit ćete upitani gdje sačuvati svako preuzimanje</string>
|
||||
<string name="downloads_storage_use_saf_title">Koristi birač sistemskih foldera (SAF)</string>
|
||||
<string name="downloads_storage_use_saf_summary">\'Okvir za pristup pohrani\' omogućava preuzimanje na eksternu SD karticu</string>
|
||||
<string name="downloads_storage_use_saf_summary_api_29">Počevši od Androida 10, podržan je samo \'Storage Access Framework\'</string>
|
||||
<string name="choose_instance_prompt">Odaberite instancu</string>
|
||||
<string name="app_language_title">Jezik aplikacije</string>
|
||||
<string name="systems_language">Zadano sistemsko</string>
|
||||
<string name="remove_watched">Ukloni gledano</string>
|
||||
<string name="remove_watched_popup_title">Ukloniti gledane videozapise?</string>
|
||||
<string name="remove_duplicates">Ukloni duplikate</string>
|
||||
<string name="remove_duplicates_title">Ukloniti duplikate?</string>
|
||||
<string name="remove_duplicates_message">Želite li ukloniti sve duplikatne tokove na ovoj listi za reprodukciju?</string>
|
||||
<string name="remove_watched_popup_warning">Videozapisi koji su pregledani prije i poslije dodavanja na listu za reprodukciju bit će uklonjeni.\nJeste li sigurni? Ovo se ne može poništiti!</string>
|
||||
<string name="remove_watched_popup_yes_and_partially_watched_videos">Da, i djelimično odgledani videozapisi</string>
|
||||
<string name="new_seek_duration_toast">Zbog ograničenja ExoPlayera, trajanje pretraživanja je postavljeno na %d sekundi</string>
|
||||
<string name="fragment_feed_title">Šta je novo</string>
|
||||
<string name="feed_group_page_summary">Stranica grupe kanala</string>
|
||||
<string name="feed_groups_header_title">Grupe kanala</string>
|
||||
<string name="feed_oldest_subscription_update">Sažetak zadnji put ažuriran: %s</string>
|
||||
<string name="feed_subscription_not_loaded_count">Nije učitano: %d</string>
|
||||
<string name="feed_notification_loading">Učitavanje feeda…</string>
|
||||
<string name="feed_processing_message">Obrada feeda…</string>
|
||||
<string name="feed_new_items">Nove stavke feeda</string>
|
||||
<string name="feed_group_dialog_select_subscriptions">Odaberite pretplate</string>
|
||||
<string name="feed_group_dialog_empty_selection">Nije odabrana pretplata</string>
|
||||
<string name="feed_group_dialog_empty_name">Prazan naziv grupe</string>
|
||||
<string name="feed_group_dialog_delete_message">Želite li izbrisati ovu grupu?</string>
|
||||
<string name="feed_create_new_group_button_title">Novo</string>
|
||||
<string name="feed_group_show_only_ungrouped_subscriptions">Prikaži samo negrupirane pretplate</string>
|
||||
<string name="settings_category_feed_title">Sažetak</string>
|
||||
<string name="feed_update_threshold_title">Prag ažuriranja feeda</string>
|
||||
<string name="feed_update_threshold_summary">Vrijeme nakon posljednjeg ažuriranja prije nego što se pretplata smatra zastarjelom — %s</string>
|
||||
<string name="feed_update_threshold_option_always_update">Uvijek ažuriraj</string>
|
||||
<string name="feed_load_error">Greška pri učitavanju feeda</string>
|
||||
<string name="feed_load_error_account_info">Nije moguće učitati feed za \'%s\'.</string>
|
||||
<string name="feed_load_error_terminated">Autorov račun je ukinut. \nNewPipe ubuduće neće moći učitavati ovaj sažetak. \nŽeliš li ukinuti pretplatu za ovaj kanal?</string>
|
||||
<string name="feed_load_error_fast_unknown">Režim brzog hranjenja ne pruža više informacija o ovome.</string>
|
||||
<string name="feed_use_dedicated_fetch_method_title">Preuzmi iz namjenskog feeda kada je dostupan</string>
|
||||
<string name="feed_use_dedicated_fetch_method_summary">Dostupno u nekim servisima, obično je mnogo brže, ali može vratiti ograničen broj artikala i često nepotpune informacije (npr. bez trajanja, vrste artikla, bez aktivnog statusa)</string>
|
||||
<string name="feed_use_dedicated_fetch_method_enable_button">Omogući brzi način rada</string>
|
||||
<string name="feed_use_dedicated_fetch_method_disable_button">Onemogući brzi način rada</string>
|
||||
<string name="feed_use_dedicated_fetch_method_help_text">Mislite li da je učitavanje feeda previše sporo? Ako je tako, pokušajte omogućiti brzo učitavanje (možete ga promijeniti u postavkama ili pritiskom na dugme ispod).\n\nNewPipe nudi dvije strategije učitavanja feeda:\n• Preuzimanje cijelog pretplatničkog kanala, što je sporo, ali potpuno.\n• Korištenje namjenske krajnje tačke usluge, što je brzo, ali obično nije potpuno.\n\nRazlika između ove dvije je u tome što brza obično nema neke informacije, poput trajanja ili vrste stavke (ne može razlikovati videozapise uživo od normalnih) i može vratiti manje stavki.\n\nYouTube je primjer usluge koja nudi ovu brzu metodu sa svojim RSS feedom.\n\nDakle, izbor se svodi na to šta preferirate: brzinu ili precizne informacije.</string>
|
||||
<string name="feed_hide_streams_title">Prikaži sljedeće tokove</string>
|
||||
<string name="feed_show_hide_streams">Prikaži/Sakrij tokove</string>
|
||||
<string name="feed_fetch_channel_tabs">Dohvati kartice kanala</string>
|
||||
<string name="feed_fetch_channel_tabs_summary">Kartice koje treba preuzeti prilikom ažuriranja feeda. Ova opcija nema efekta ako se kanal ažurira pomoću brzog načina rada.</string>
|
||||
<string name="content_not_supported">Ovaj sadržaj još uvijek nije podržan od strane NewPipe-a.\n\nNadamo se da će biti podržan u budućoj verziji.</string>
|
||||
<string name="detail_sub_channel_thumbnail_view_description">Sličica avatara kanala</string>
|
||||
<string name="channel_created_by">Kreirao/la %s</string>
|
||||
<string name="video_detail_by">Napisao %s</string>
|
||||
<string name="playlist_page_summary">Stranica s popisom za reprodukciju</string>
|
||||
<string name="show_thumbnail_title">Prikaži sličicu</string>
|
||||
<string name="show_thumbnail_summary">Koristite sličicu i za pozadinu zaključanog ekrana i za obavještenja</string>
|
||||
<string name="recent">Nedavno</string>
|
||||
<string name="chapters">Poglavlja</string>
|
||||
<string name="no_app_to_open_intent">Nijedna aplikacija na vašem uređaju ne može ovo otvoriti</string>
|
||||
<string name="no_appropriate_file_manager_message">Nije pronađen odgovarajući upravitelj datoteka za ovu radnju.\nMolimo instalirajte upravitelj datoteka ili pokušajte onemogućiti \'%s\' u postavkama preuzimanja</string>
|
||||
<string name="no_appropriate_file_manager_message_android_10">Nije pronađen odgovarajući upravitelj datoteka za ovu radnju.\nMolimo instalirajte upravitelj datoteka kompatibilan sa Storage Access Frameworkom</string>
|
||||
<string name="georestricted_content">Ovaj sadržaj nije dostupan u vašoj zemlji.</string>
|
||||
<string name="soundcloud_go_plus_content">Ovo je pjesma na SoundCloud Go+ platformi, barem u vašoj zemlji, tako da je NewPipe ne može strimovati ili preuzeti.</string>
|
||||
<string name="private_content">Ovaj sadržaj je privatan, tako da ga NewPipe ne može strimovati ili preuzimati.</string>
|
||||
<string name="youtube_music_premium_content">Ovaj video je dostupan samo članovima YouTube Music Premium-a, tako da ga NewPipe ne može strimovati ili preuzeti.</string>
|
||||
<string name="account_terminated">Račun ukinut</string>
|
||||
<string name="account_terminated_service_provides_reason">Račun ukinut\n\n%1$s navodi ovaj razlog: %2$s</string>
|
||||
<string name="paid_content">Ovaj sadržaj je dostupan samo korisnicima koji su platili, tako da ga NewPipe ne može strimovati ili preuzimati.</string>
|
||||
<string name="featured">Istaknuto</string>
|
||||
<string name="radio">Radio</string>
|
||||
<string name="auto_device_theme_title">Automatski (tema uređaja)</string>
|
||||
<string name="night_theme_summary">Odaberite svoju omiljenu noćnu temu — %s</string>
|
||||
<string name="select_night_theme_toast">Možete odabrati svoju omiljenu noćnu temu ispod</string>
|
||||
<string name="night_theme_available">Ova opcija je dostupna samo ako je za temu odabrana %s</string>
|
||||
<string name="download_has_started">Preuzimanje je počelo</string>
|
||||
<string name="description_select_note">Sada možete odabrati tekst unutar opisa. Imajte na umu da stranica može treperiti i da linkovi možda neće biti dostupni za klikanje dok ste u načinu odabira.</string>
|
||||
<string name="description_select_enable">Omogući odabir teksta u opisu</string>
|
||||
<string name="description_select_disable">Onemogući odabir teksta u opisu</string>
|
||||
<string name="metadata_category">Kategorija</string>
|
||||
<string name="metadata_tags">Oznake</string>
|
||||
<string name="metadata_licence">Dozvola</string>
|
||||
<string name="metadata_privacy">Privatnost</string>
|
||||
<string name="metadata_age_limit">Starosna granica</string>
|
||||
<string name="metadata_language">Jezik</string>
|
||||
<string name="metadata_support">Podrška</string>
|
||||
<string name="metadata_host">Domaćin</string>
|
||||
<string name="metadata_thumbnails">Sličice</string>
|
||||
<string name="metadata_uploader_avatars">Avatari koji su postavili profil</string>
|
||||
<string name="metadata_subchannel_avatars">Avatari podkanala</string>
|
||||
<string name="metadata_avatars">Avatari</string>
|
||||
<string name="metadata_banners">Baneri</string>
|
||||
<string name="metadata_privacy_public">Javno</string>
|
||||
<string name="metadata_privacy_unlisted">Nije navedeno</string>
|
||||
<string name="metadata_privacy_private">Privatno</string>
|
||||
<string name="metadata_privacy_internal">Unutrašnje</string>
|
||||
<string name="metadata_subscribers">Pretplatnici</string>
|
||||
<string name="detail_pinned_comment_view_description">Zakačen komentar</string>
|
||||
<string name="detail_heart_img_view_description">Srce od strane kreatora</string>
|
||||
<string name="open_website_license">Otvori web stranicu</string>
|
||||
<string name="tablet_mode_title">Tabletni način rada</string>
|
||||
<string name="on">Upaljeno</string>
|
||||
<string name="off">Ugašeno</string>
|
||||
<string name="progressive_load_interval_exoplayer_default">Zadano za ExoPlayer</string>
|
||||
<string name="notifications_disabled">Obavještenja su onemogućena</string>
|
||||
<string name="get_notified">Primajte obavještenja</string>
|
||||
<string name="you_successfully_subscribed">Sada ste pretplaćeni na ovaj kanal</string>
|
||||
<string name="enumeration_comma">,</string>
|
||||
<string name="toggle_all">Prikaži/Uključi sve</string>
|
||||
<string name="streams_not_yet_supported_removed">Strimovi koje program za preuzimanje još ne podržava nisu prikazani</string>
|
||||
<string name="audio_track_present_in_video">Zvučni zapis bi već trebao biti prisutan u ovom toku</string>
|
||||
<string name="selected_stream_external_player_not_supported">Odabrani tok nije podržan od strane eksternih plejera</string>
|
||||
<string name="no_audio_streams_available_for_external_players">Nema dostupnih audio tokova za vanjske uređaje za reprodukciju</string>
|
||||
<string name="no_video_streams_available_for_external_players">Nema video tokova dostupnih za vanjske uređaje za reprodukciju</string>
|
||||
<string name="select_quality_external_players">Odaberite kvalitet za vanjske uređaje za reprodukciju</string>
|
||||
<string name="select_audio_track_external_players">Odaberite audio zapis za vanjske uređaje za reprodukciju</string>
|
||||
<string name="unknown_format">Nepoznati format</string>
|
||||
<string name="unknown_quality">Nepoznat kvalitet</string>
|
||||
<string name="unknown_audio_track">Nepoznato</string>
|
||||
<string name="feed_show_watched">Potpuno odgledano</string>
|
||||
<string name="feed_show_partially_watched">Djelomično gledano</string>
|
||||
<string name="feed_show_upcoming">Nadolazeći</string>
|
||||
<string name="sort">Sortiraj</string>
|
||||
<string name="settings_category_exoplayer_title">Postavke ExoPlayera</string>
|
||||
<string name="settings_category_exoplayer_summary">Upravljajte nekim postavkama ExoPlayera. Ove promjene zahtijevaju ponovno pokretanje plejera da bi stupile na snagu</string>
|
||||
<string name="use_exoplayer_decoder_fallback_title">Koristite ExoPlayer-ovu rezervnu funkciju dekodera</string>
|
||||
<string name="use_exoplayer_decoder_fallback_summary">Omogućite ovu opciju ako imate problema s inicijalizacijom dekodera, koja se vraća na dekodere nižeg prioriteta ako inicijalizacija primarnih dekodera ne uspije. Ovo može rezultirati lošijim performansama reprodukcije nego kada se koriste primarni dekoderi</string>
|
||||
<string name="always_use_exoplayer_set_output_surface_workaround_title">Uvijek koristite ExoPlayer-ovo rješenje za podešavanje površine video izlaza</string>
|
||||
<string name="always_use_exoplayer_set_output_surface_workaround_summary">Ovo zaobilazno rješenje oslobađa i ponovo instancira video kodeke kada dođe do promjene površine, umjesto direktnog postavljanja površine na kodek. Već korištena od strane ExoPlayera na nekim uređajima s ovim problemom, ova postavka ima učinak samo na Androidu 6 i novijim verzijama.\n\nOmogućavanje ove opcije može spriječiti greške u reprodukciji prilikom prebacivanja trenutnog video playera ili prelaska na cijeli ekran</string>
|
||||
<string name="audio_track_name">%1$s %2$s</string>
|
||||
<string name="audio_track_type_original">original</string>
|
||||
<string name="audio_track_type_dubbed">sinhronizovano</string>
|
||||
<string name="audio_track_type_descriptive">opisni</string>
|
||||
<string name="audio_track_type_secondary">sekundarni</string>
|
||||
<string name="channel_tab_videos">Videozapisi</string>
|
||||
<string name="channel_tab_tracks">Snimke</string>
|
||||
<string name="channel_tab_shorts">Kratke hlače</string>
|
||||
<string name="channel_tab_livestreams">Uživo</string>
|
||||
<string name="channel_tab_channels">Kanali</string>
|
||||
<string name="channel_tab_playlists">Plejliste</string>
|
||||
<string name="channel_tab_albums">Albuma</string>
|
||||
<string name="channel_tab_likes">Sviđanja</string>
|
||||
<string name="channel_tab_about">O tome</string>
|
||||
<string name="show_channel_tabs">Kartice kanala</string>
|
||||
<string name="show_channel_tabs_summary">Koje kartice se prikazuju na stranicama kanala</string>
|
||||
<string name="open_play_queue">Otvori red za reprodukciju</string>
|
||||
<string name="toggle_fullscreen">Prikaz preko cijelog ekrana</string>
|
||||
<string name="toggle_screen_orientation">Uključi/isključi orijentaciju ekrana</string>
|
||||
<string name="previous_stream">Prethodni tok</string>
|
||||
<string name="next_stream">Sljedeći tok</string>
|
||||
<string name="play">Pokrenuti</string>
|
||||
<string name="replay">Ponovna reprodukcija</string>
|
||||
<string name="more_options">Više opcija</string>
|
||||
<string name="duration">Trajanje</string>
|
||||
<string name="rewind">Premotavanje unazad</string>
|
||||
<string name="forward">Naprijed</string>
|
||||
<string name="image_quality_title">Kvalitet slike</string>
|
||||
<string name="image_quality_summary">Odaberite kvalitet slika i da li će se slike uopće učitavati kako biste smanjili potrošnju podataka i memorije. Promjene brišu keš memoriju slika i u memoriji i na disku — %s</string>
|
||||
<string name="image_quality_none">Ne učitavaj slike</string>
|
||||
<string name="image_quality_low">Niska kvaliteta</string>
|
||||
<string name="image_quality_medium">Srednji kvalitet</string>
|
||||
<string name="image_quality_high">Visoka kvaliteta</string>
|
||||
<string name="question_mark">\?</string>
|
||||
<string name="share_playlist">Dijeli plejlistu</string>
|
||||
<string name="share_playlist_with_titles">Podijeli s naslovima</string>
|
||||
<string name="share_playlist_with_list">Podijeli listu URL-ova</string>
|
||||
<string name="share_playlist_as_youtube_temporary_playlist">Podijeli kao privremenu YouTube plejlistu</string>
|
||||
<string name="video_details_list_item">- %1$s: %2$s</string>
|
||||
<string name="share_playlist_content_details">%1$s\n%2$s</string>
|
||||
<string name="show_more">Prikaži više</string>
|
||||
<string name="show_less">Prikaži manje</string>
|
||||
<string name="import_settings_vulnerable_format">Postavke u izvozu koji se uvozi koriste ranjivi format koji je zastario od verzije NewPipe 0.27.0. Provjerite da izvoz koji se uvozi dolazi iz pouzdanog izvora i u budućnosti preferirajte korištenje samo izvoza dobivenih iz NewPipe 0.27.0 ili novije verzije. Podrška za uvoz postavki u ovom ranjivom formatu uskoro će biti potpuno uklonjena, a zatim stare verzije NewPipe-a više neće moći uvoziti postavke izvoza iz novih verzija.</string>
|
||||
<string name="migration_info_6_7_title">Stranica SoundCloud Top 50 uklonjena</string>
|
||||
<string name="migration_info_6_7_message">SoundCloud je ukinuo originalne Top 50 liste. Odgovarajuća kartica je uklonjena sa vaše glavne stranice.</string>
|
||||
<string name="migration_info_7_8_title">Uklonjen je kombinovani prikaz trendova na YouTubeu</string>
|
||||
<string name="trending_gaming">Trendovi u igrama</string>
|
||||
<string name="trending_podcasts">Trendovi podcasti</string>
|
||||
<string name="trending_movies">Popularni filmovi i serije</string>
|
||||
<string name="trending_music">Popularna muzika</string>
|
||||
<string name="entry_deleted">Unos izbrisan</string>
|
||||
<string name="player_http_403">HTTP greška 403 primljena od servera tokom reprodukcije, vjerovatno uzrokovana istekom URL-a za tokove ili zabranom IP adrese</string>
|
||||
<string name="player_http_invalid_status">HTTP greška %1$s primljena od servera tokom reprodukcije</string>
|
||||
<string name="youtube_player_http_403">HTTP greška 403 primljena od servera tokom reprodukcije, vjerovatno uzrokovana zabranom IP adrese ili problemima s deobfuskacijom URL-a za tokove</string>
|
||||
<string name="sign_in_confirm_not_bot_error">%1$s je odbio dati podatke, tražeći prijavu kako bi potvrdio da podnosilac zahtjeva nije bot.\n\nVašu IP adresu je možda privremeno zabranio %1$s, možete pričekati neko vrijeme ili preći na drugu IP adresu (na primjer uključivanjem/isključivanjem VPN-a ili prelaskom s WiFi-ja na mobilne podatke).</string>
|
||||
<plurals name="subscribers">
|
||||
<item quantity="one">%s pretplatnik</item>
|
||||
<item quantity="few">%s pretplatnika</item>
|
||||
<item quantity="other">%s pretplatnika</item>
|
||||
</plurals>
|
||||
<plurals name="views">
|
||||
<item quantity="one">%s pregled</item>
|
||||
<item quantity="few">%s pregleda</item>
|
||||
<item quantity="other">%s pregleda</item>
|
||||
</plurals>
|
||||
<plurals name="watching">
|
||||
<item quantity="one">%s gledatelj</item>
|
||||
<item quantity="few">%s gledatelja</item>
|
||||
<item quantity="other">%s gledatelja</item>
|
||||
</plurals>
|
||||
<plurals name="listening">
|
||||
<item quantity="one">%s slušatelj</item>
|
||||
<item quantity="few">%s slušatelja</item>
|
||||
<item quantity="other">%s slušatelja</item>
|
||||
</plurals>
|
||||
<plurals name="videos">
|
||||
<item quantity="one">%s video</item>
|
||||
<item quantity="few">%s videozapisa</item>
|
||||
<item quantity="other">%s videozapisa</item>
|
||||
</plurals>
|
||||
<plurals name="new_streams">
|
||||
<item quantity="one">%s novi tok</item>
|
||||
<item quantity="few">%s nova toka</item>
|
||||
<item quantity="other">%s novih tokova</item>
|
||||
</plurals>
|
||||
<string name="tab_about">O aplikaciji i pitanja</string>
|
||||
<plurals name="download_finished_notification">
|
||||
<item quantity="one">%s preuzimanje je gotovo</item>
|
||||
<item quantity="few">%s preuzimanja su gotova</item>
|
||||
<item quantity="other">%s preuzimanja je gotovo</item>
|
||||
</plurals>
|
||||
<plurals name="deleted_downloads_toast">
|
||||
<item quantity="one">Izbrisano %1$s preuzimanje</item>
|
||||
<item quantity="few">Izbrisana %1$s preuzimanja</item>
|
||||
<item quantity="other">Izbrisano %1$s preuzimanja</item>
|
||||
</plurals>
|
||||
<plurals name="seconds">
|
||||
<item quantity="one">%d sekunda</item>
|
||||
<item quantity="few">%d sekunde</item>
|
||||
<item quantity="other">%d sekundi</item>
|
||||
</plurals>
|
||||
<plurals name="minutes">
|
||||
<item quantity="one">%d minut</item>
|
||||
<item quantity="few">%d minute</item>
|
||||
<item quantity="other">%d minuta</item>
|
||||
</plurals>
|
||||
<plurals name="hours">
|
||||
<item quantity="one">%d sat</item>
|
||||
<item quantity="few">%d sata</item>
|
||||
<item quantity="other">%d sati</item>
|
||||
</plurals>
|
||||
<plurals name="days">
|
||||
<item quantity="one">%d dan</item>
|
||||
<item quantity="few">%d dana</item>
|
||||
<item quantity="other">%d dana</item>
|
||||
</plurals>
|
||||
<plurals name="feed_group_dialog_selection_count">
|
||||
<item quantity="one">%d odabrana</item>
|
||||
<item quantity="few">%d odabrane</item>
|
||||
<item quantity="other">%d odabranih</item>
|
||||
</plurals>
|
||||
<plurals name="replies">
|
||||
<item quantity="one">%s odgovor</item>
|
||||
<item quantity="few">%s odgovora</item>
|
||||
<item quantity="other">%s odgovora</item>
|
||||
</plurals>
|
||||
<string name="migration_info_7_8_message">YouTube je ukinuo kombinovanu stranicu s trendovima od 21. jula 2025. NewPipe je zamijenio zadanu stranicu s trendovima s trendovima uživo prijenosa.\n\nTakođer možete odabrati različite stranice s trendovima u \"Postavke > Sadržaj > Sadržaj glavne stranice\".</string>
|
||||
<string name="unsupported_content_in_country">Ovaj sadržaj nije dostupan za trenutno odabranu zemlju sadržaja.\n\nPromijenite svoj odabir u \"Postavke > Sadržaj > Zadana zemlja sadržaja\".</string>
|
||||
</resources>
|
||||
|
||||
@@ -215,7 +215,7 @@
|
||||
<string name="playback_speed_control">Controls de la velocitat de reproducció</string>
|
||||
<string name="playback_tempo">Tempo</string>
|
||||
<string name="playback_pitch">To</string>
|
||||
<string name="main_bg_subtitle">Toca \"Cerca\" per començar.</string>
|
||||
<string name="main_bg_subtitle">Toqueu la lupa per començar.</string>
|
||||
<string name="use_external_video_player_summary">Elimina l\'àudio en algunes resolucions</string>
|
||||
<string name="use_external_audio_player_title">Reproductor d\'àudio extern</string>
|
||||
<string name="enable_search_history_summary">Emmagatzema les cerques localment</string>
|
||||
|
||||
@@ -486,7 +486,7 @@
|
||||
</plurals>
|
||||
<string name="feed_group_dialog_empty_name">Prázdné jméno skupiny</string>
|
||||
<string name="feed_group_dialog_delete_message">Přejete si odstranit tuto skupinu?</string>
|
||||
<string name="feed_create_new_group_button_title">Nová</string>
|
||||
<string name="feed_create_new_group_button_title">Nový</string>
|
||||
<string name="settings_category_feed_title">Novinky</string>
|
||||
<string name="feed_update_threshold_title">Limit aktualizace novinek</string>
|
||||
<string name="feed_update_threshold_summary">Doba po poslední aktualizaci, po níž je odběr považován za zastaralý — %s</string>
|
||||
@@ -717,7 +717,7 @@
|
||||
<string name="app_update_available_notification_text">Klepnutím stáhnete %s</string>
|
||||
<string name="fast_mode">Rychlý režim</string>
|
||||
<string name="app_update_unavailable_toast">Používáte nejnovější verzi NewPipe</string>
|
||||
<string name="import_subscriptions_hint">Import nebo export odběrů z 3-tečkové nabídky</string>
|
||||
<string name="import_subscriptions_hint">Importujte nebo exportujte odběry z 3tečkové nabídky</string>
|
||||
<string name="night_theme_available">Tato možnost je dostupná pouze při vybraném motivu %s</string>
|
||||
<string name="unset_playlist_thumbnail">Zrušení nastavení trvalého náhledu</string>
|
||||
<string name="card">Karta</string>
|
||||
@@ -842,4 +842,24 @@
|
||||
<string name="channel_tab_likes">Líbí se</string>
|
||||
<string name="migration_info_6_7_title">Stránka SoundCloud Top 50 odstraněna</string>
|
||||
<string name="migration_info_6_7_message">SoundCloud zrušil původní žebříčky Top 50. Příslušná karta byla odstraněna z vaší hlavní stránky.</string>
|
||||
<string name="migration_info_7_8_title">YouTube kombinované trendy odstraněny</string>
|
||||
<string name="migration_info_7_8_message">YouTube ukončil provoz kombinované stránky s trendy k 21. červenci 2025. NewPipe nahradil výchozí stránku s trendy stránkou s trendy živými přenosy.\n\nMůžete také vybrat různé stránky s trendy v části „Nastavení > Obsah > Obsah úvodní stránky“.</string>
|
||||
<string name="trending_gaming">Populární hry</string>
|
||||
<string name="trending_podcasts">Populární podcasty</string>
|
||||
<string name="trending_movies">Populární filmy a seriály</string>
|
||||
<string name="trending_music">Populární hudba</string>
|
||||
<string name="short_thousand">%s tis.</string>
|
||||
<string name="short_million">%s mil.</string>
|
||||
<string name="short_billion">%s mld.</string>
|
||||
<string name="permission_display_over_apps_message">Pro používání Popup Playeru vyberte v následující nabídce nastavení Androidu možnost %1$s a povolte %2$s.</string>
|
||||
<string name="permission_display_over_apps_permission_name">\"Povolit zobrazení přes jiné aplikace\"</string>
|
||||
<string name="delete_file">Vymazat soubor</string>
|
||||
<string name="delete_entry">Vymazat položku</string>
|
||||
<string name="entry_deleted">Položka vymazána</string>
|
||||
<string name="account_terminated_service_provides_reason">Ukončení účtu\n\n%1$s uvádí tento důvod: %2$s</string>
|
||||
<string name="player_http_403">Během přehrávání byla ze serveru přijata chyba HTTP 403, pravděpodobně způsobená vypršením platnosti streamingové adresy URL nebo zákazem IP adresy</string>
|
||||
<string name="player_http_invalid_status">Chyba HTTP %1$s obdržená ze serveru během přehrávání</string>
|
||||
<string name="youtube_player_http_403">Chyba HTTP 403 obdržená od serveru během přehrávání, pravděpodobně způsobená zákazem IP adresy nebo problémy s deobfuskací streamovací adresy URL</string>
|
||||
<string name="sign_in_confirm_not_bot_error">%1$s odmítl poskytnout data, žádá o přihlášení k potvrzení, že žadatel není bot.\n\nVaše IP adresa mohla být dočasně zakázána %1$s, můžete nějakou dobu počkat nebo přepnout na jinou IP adresu (například zapnutím/vypnutím VPN nebo přepnutím z WiFi na mobilní data).</string>
|
||||
<string name="unsupported_content_in_country">Tento obsah není pro aktuálně vybranou zemi obsahu dostupný.\n\nZměňte výběr v nabídce \"Nastavení > Obsah > Výchozí země obsahu\".</string>
|
||||
</resources>
|
||||
|
||||
@@ -232,7 +232,7 @@
|
||||
<string name="title_most_played">Mest Afspillet</string>
|
||||
<string name="main_page_content">Indhold af hovedside</string>
|
||||
<string name="main_page_content_summary">Hvilke faner vises på hovedsiden</string>
|
||||
<string name="blank_page_summary">Tom Side</string>
|
||||
<string name="blank_page_summary">Tom side</string>
|
||||
<string name="kiosk_page_summary">Kioskside</string>
|
||||
<string name="channel_page_summary">Kanalside</string>
|
||||
<string name="select_a_channel">Vælg en kanal</string>
|
||||
@@ -823,4 +823,13 @@
|
||||
<string name="feed_group_page_summary">Kanalgruppeside</string>
|
||||
<string name="select_a_feed_group">Vælg en feed-gruppe</string>
|
||||
<string name="no_feed_group_created_yet">Ingen feed-gruppe oprettet endnu</string>
|
||||
<string name="search_with_service_name">Søg %1$s</string>
|
||||
<string name="search_with_service_name_and_filter">Søg %1$s (%2$s)</string>
|
||||
<string name="permission_display_over_apps_message">For at kunne bruge pop op-afspilleren skal du vælge %1$s i følgende Android-indstillingsmenu og aktivere %2$s.</string>
|
||||
<string name="permission_display_over_apps_permission_name">“Tillad visning over andre apps”</string>
|
||||
<string name="short_thousand">%sK</string>
|
||||
<string name="short_million">%sM</string>
|
||||
<string name="short_billion">%sB</string>
|
||||
<string name="delete_file">Slet fil</string>
|
||||
<string name="account_terminated_service_provides_reason">Kontoen er blevet lukket\n\n%1$s angiver følgende årsag: %2$s</string>
|
||||
</resources>
|
||||
|
||||
@@ -828,4 +828,24 @@
|
||||
<string name="channel_tab_likes">Gefällt mir</string>
|
||||
<string name="migration_info_6_7_title">SoundCloud-Top-50-Seite entfernt</string>
|
||||
<string name="migration_info_6_7_message">SoundCloud hat die ursprünglichen Top-50-Charts abgeschafft. Der entsprechende Tab wurde von deiner Hauptseite entfernt.</string>
|
||||
<string name="short_million">%sMio.</string>
|
||||
<string name="short_billion">%sMrd.</string>
|
||||
<string name="short_thousand">%sTsd.</string>
|
||||
<string name="trending_gaming">Gaming-Trends</string>
|
||||
<string name="trending_movies">Beliebte Filme und Shows</string>
|
||||
<string name="trending_music">Beliebte Musik</string>
|
||||
<string name="trending_podcasts">Beliebte Podcasts</string>
|
||||
<string name="migration_info_7_8_title">YouTube hat den geteilten Feed entfernt</string>
|
||||
<string name="migration_info_7_8_message">YouTube hat die kombinierte Trending-Seite ab dem 21. Juli 2025 eingestellt. NewPipe hat die Standard-Trending-Seite durch die Trending-Livestreams ersetzt.\n\nDu kannst auch verschiedene Trendseiten unter „Einstellungen > Inhalt > Inhalt der Hauptseite“ auswählen.</string>
|
||||
<string name="permission_display_over_apps_message">Um den Pop-up-Player zu verwenden, bitte in den folgenden Android-Einstellungen %1$s auswählen und %2$s aktivieren.</string>
|
||||
<string name="permission_display_over_apps_permission_name">„Über anderen Apps einblenden“</string>
|
||||
<string name="delete_file">Datei löschen</string>
|
||||
<string name="delete_entry">Eintrag löschen</string>
|
||||
<string name="entry_deleted">Eintrag gelöscht</string>
|
||||
<string name="account_terminated_service_provides_reason">Konto geschlossen\n\n%1$s gibt folgenden Grund an: %2$s</string>
|
||||
<string name="player_http_403">HTTP-Fehler 403 vom Server während der Wiedergabe erhalten, wahrscheinlich verursacht durch Ablauf der Streaming-URL oder eine IP-Sperre</string>
|
||||
<string name="player_http_invalid_status">HTTP-Fehler %1$s vom Server während der Wiedergabe erhalten</string>
|
||||
<string name="youtube_player_http_403">HTTP-Fehler 403 vom Server während der Wiedergabe erhalten, wahrscheinlich verursacht durch eine IP-Sperre oder Probleme beim Entschlüsseln der Streaming-URL</string>
|
||||
<string name="sign_in_confirm_not_bot_error">%1$s hat die Datenbereitstellung verweigert und verlangt eine Anmeldung, um zu bestätigen, dass es sich bei dem Anfragenden nicht um einen Bot handelt.\n\nDeine IP-Adresse wurde möglicherweise vorübergehend von %1$s gesperrt. Du kannst einige Zeit warten oder zu einer anderen IP-Adresse wechseln (z. B. durch Ein- und Ausschalten eines VPNs oder durch Wechseln von WLAN zu mobilen Daten).</string>
|
||||
<string name="unsupported_content_in_country">Dieser Inhalt ist für das aktuell ausgewählte Land des Inhalts nicht verfügbar.\n\nÄndere die Auswahl unter „Einstellungen > Inhalt > Bevorzugtes Land des Inhalts“.</string>
|
||||
</resources>
|
||||
|
||||
@@ -825,7 +825,27 @@
|
||||
<string name="feed_group_page_summary">Σελίδα καναλιού ομάδας</string>
|
||||
<string name="search_with_service_name">Αναζήτηση %1$s</string>
|
||||
<string name="search_with_service_name_and_filter">Αναζήτηση %1$s (%2$s)</string>
|
||||
<string name="channel_tab_likes">Likes</string>
|
||||
<string name="channel_tab_likes">Μου αρέσει</string>
|
||||
<string name="migration_info_6_7_title">Η σελίδα των SoundCloud Top 50 αφαιρέθηκε</string>
|
||||
<string name="migration_info_6_7_message">Το SoundCloud έχει καταργήσει τα αρχικά charts με τα Top 50. Η αντίστοιχη καρτέλα έχει αφαιρεθεί από την κύρια σελίδα σας.</string>
|
||||
<string name="migration_info_7_8_title">Οι συνδυασμένες τάσεις στο YouTube καταργήθηκαν</string>
|
||||
<string name="migration_info_7_8_message">Το YouTube έχει καταργήσει τη συνδυασμένη σελίδα με τάσεις από την 21 Ιουλίου 2025. Το NewPipe αντικατέστησε την προεπιλεγμένη σελίδα τάσεων με τις ζωντανές ροές τάσεων.\n\nΜπορείτε επίσης να επιλέξετε διαφορετικές σελίδες με τάσεις στις \"Ρυθμίσεις > Περιεχόμενο > Περιεχόμενο κύριας σελίδας\".</string>
|
||||
<string name="trending_gaming">Τάσεις παιχνιδιών</string>
|
||||
<string name="trending_podcasts">Τάσεις podcasts</string>
|
||||
<string name="trending_movies">Τάσεις ταινιών και εκπομπών</string>
|
||||
<string name="trending_music">Μουσικές τάσεις</string>
|
||||
<string name="short_thousand">%sK</string>
|
||||
<string name="short_million">%sM</string>
|
||||
<string name="short_billion">%sB</string>
|
||||
<string name="permission_display_over_apps_message">Για να χρησιμοποιήσετε το Αναδυόμενο Πρόγραμμα Αναπαραγωγής, επιλέξτε %1$s στο ακόλουθο μενού ρυθμίσεων Android και ενεργοποιήστε το %2$s.</string>
|
||||
<string name="permission_display_over_apps_permission_name">«Να επιτρέπεται η εμφάνιση πάνω από άλλες εφαρμογές»</string>
|
||||
<string name="delete_file">Διαγραφή αρχείου</string>
|
||||
<string name="delete_entry">Διαγραφή καταχώρησης</string>
|
||||
<string name="entry_deleted">Η καταχώρηση διαγράφηκε</string>
|
||||
<string name="account_terminated_service_provides_reason">Ο λογαριασμός έκλεισε\n\n%1$s παρέχει αυτήν την αιτία: %2$s</string>
|
||||
<string name="player_http_403">Σφάλμα HTTP 403 που ελήφθη από τον διακομιστή κατά την αναπαραγωγή, πιθανώς λόγω λήξης διεύθυνσης URL ροής ή αποκλεισμού IP</string>
|
||||
<string name="player_http_invalid_status">Σφάλμα HTTP %1$s ελήφθη από τον διακομιστή κατά την αναπαραγωγή</string>
|
||||
<string name="youtube_player_http_403">Σφάλμα HTTP 403 ελήφθη από τον διακομιστή κατά την αναπαραγωγή, πιθανώς λόγω αποκλεισμού IP ή προβλημάτων απεμπλοκής URL ροής</string>
|
||||
<string name="sign_in_confirm_not_bot_error">Ο %1$s αρνήθηκε να παράσχει δεδομένα, ζητώντας σύνδεση για να επιβεβαιώσει ότι ο αιτών δεν είναι bot.\n\nΗ IP σας ενδέχεται να έχει αποκλειστεί προσωρινά από τον %1$s. Μπορείτε να περιμένετε λίγο ή να αλλάξετε IP (για παράδειγμα, ενεργοποιώντας/απενεργοποιώντας ένα VPN ή αλλάζοντας από WiFi σε δεδομένα κινητής τηλεφωνίας).</string>
|
||||
<string name="unsupported_content_in_country">Αυτό το περιεχόμενο δεν είναι διαθέσιμο για την τρέχουσα επιλεγμένη χώρα περιεχομένου.\n\nΑλλάξτε την επιλογή σας από \"Ρυθμίσεις > Περιεχόμενο > Προεπιλεγμένη χώρα περιεχομένου\".</string>
|
||||
</resources>
|
||||
|
||||
@@ -84,4 +84,14 @@
|
||||
<string name="ok">Okay</string>
|
||||
<string name="open_in_browser">Open in browser</string>
|
||||
<string name="no_player_found_toast">No stream player found (you can install VLC to play it).</string>
|
||||
<string name="yes">Yes</string>
|
||||
<string name="no">No</string>
|
||||
<string name="mark_as_watched">Mark as watched</string>
|
||||
<string name="open_in_popup_mode">Open in popup mode</string>
|
||||
<string name="open_with">Open with</string>
|
||||
<string name="share">Share</string>
|
||||
<string name="download">Download</string>
|
||||
<string name="controls_download_desc">Download stream file</string>
|
||||
<string name="search">Search</string>
|
||||
<string name="settings">Settings</string>
|
||||
</resources>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user